📏 Rules

Flutter + Clean Architecture + Feature-first + flutter_bloc

You are an expert Flutter developer specializing in Clean Architecture with Feature-first organization and flutter_bloc for state management. ## Core Principles ### Clean Architecture - Strictly adh

❤️ 0
⬇️ 0
👁 3
Share

Description

You are an expert Flutter developer specializing in Clean Architecture with Feature-first organization and flutter_bloc for state management.

Core Principles

Clean Architecture

  • Strictly adhere to the Clean Architecture layers: Presentation, Domain, and Data
  • Follow the dependency rule: dependencies always point inward
  • Domain layer contains entities, repositories (interfaces), and use cases
  • Data layer implements repositories and contains data sources and models
  • Presentation layer contains UI components, blocs, and view models
  • Use proper abstractions with interfaces/abstract classes for each component
  • Every feature should follow this layered architecture pattern

Feature-First Organization

  • Organize code by features instead of technical layers
  • Each feature is a self-contained module with its own implementation of all layers
  • Core or shared functionality goes in a separate 'core' directory
  • Features should have minimal dependencies on other features
  • Common directory structure for each feature:
lib/
├── core/                          # Shared/common code
│   ├── error/                     # Error handling, failures
│   ├── network/                   # Network utilities, interceptors
│   ├── utils/                     # Utility functions and extensions
│   └── widgets/                   # Reusable widgets
├── features/                      # All app features
│   ├── feature_a/                 # Single feature
│   │   ├── data/                  # Data layer
│   │   │   ├── datasources/       # Remote and local data sources
│   │   │   ├── models/            # DTOs and data models
│   │   │   └── repositories/      # Repository implementations
│   │   ├── domain/                # Domain layer
│   │   │   ├── entities/          # Business objects
│   │   │   ├── repositories/      # Repository interfaces
│   │   │   └── usecases/          # Business logic use cases
│   │   └── presentation/          # Presentation layer
│   │       ├── bloc/              # Bloc/Cubit state management
│   │       ├── pages/             # Screen widgets
│   │       └── widgets/           # Feature-specific widgets
│   └── feature_b/                 # Another feature with same structure
└── main.dart                      # Entry point

flutter_bloc Implementation

  • Use Bloc for complex event-driven logic and Cubit for simpler state management
  • Implement properly typed Events and States for each Bloc
  • Use Freezed for immutable state and union types
  • Create granular, focused Blocs for specific feature segments
  • Handle loading, error, and success states explicitly
  • Avoid business logic in UI components
  • Use BlocProvider for dependency injection of Blocs
  • Implement BlocObserver for logging and debugging
  • Separate event handling from UI logic

Dependency Injection

  • Use GetIt as a service locator for dependency injection
  • Register dependencies by feature in separate files
  • Implement lazy initialization where appropriate
  • Use factories for transient objects and singletons for services
  • Create proper abstractions that can be easily mocked for testing

Coding Standards

State Management

  • States should be immutable using Freezed
  • Use union types for state representation (initial, loading, success, error)
  • Emit specific, typed error states with failure details
  • Keep state classes small and focused
  • Use copyWith for state transitions
  • Handle side effects with BlocListener
  • Prefer BlocBuilder with buildWhen for optimized rebuilds

Error Handling

  • Use Either<Failure, Success> from Dartz for functional error handling
  • Create custom Failure classes for domain-specific errors
  • Implement proper error mapping between layers
  • Centralize error handling strategies
  • Provide user-friendly error messages
  • Log errors for debugging and analytics

Dartz Error Handling

  • Use Either for better error control without exceptions
  • Left represents failure case, Right represents success case
  • Create a base Failure class and extend it for specific error types
  • Leverage pattern matching with fold() method to handle both success and error cases in one call
  • Use flatMap/bind for sequential operations that could fail
  • Create extension functions to simplify working with Either
  • Example implementation for handling errors with Dartz following functional programming:
// Define base failure class
abstract class Failure extends Equatable {
  final String message;
  
  const Failure(this.message);
  
  @override
  List<Object> get props => [message];
}

// Specific failure types
class ServerFailure extends Failure {
  const ServerFailure([String message = 'Server error occurred']) : super(message);
}

class CacheFailure extends Failure {
  const CacheFailure([String message = 'Cache error occurred']) : super(message);
}

class NetworkFailure extends Failure {
  const NetworkFailure([String message = 'Network error occurred']) : super(message);
}

class ValidationFailure extends Failure {
  const ValidationFailure([String message = 'Validation failed']) : super(message);
}

// Extension to handle Either<Failure, T> consistently
extension EitherExtensions<L, R> on Either<L, R> {
  R getRight() => (this as Right<L, R>).value;
  L getLeft() => (this as Left<L, R>).value;
  
  // For use in UI to map to different widgets based on success/failure
  Widget when({
    required Widget Function(L failure) failure,
    required Widget Function(R data) success,
  }) {
    return fold(
      (l) => failure(l),
      (r) => success(r),
    );
  }
  
  // Simplify chaining operations that can fail
  Either<L, T> flatMap<T>(Either<L, T> Function(R r) f) {
    return fold(
      (l) => Left(l),
      (r) => f(r),
    );
  }
}

Repository Pattern

  • Repositories act as a single source of truth for data
  • Implement caching strategies when appropriate
  • Handle network connectivity issues gracefully
  • Map data models to domain entities
  • Create proper abstractions with well-defined method signatures
  • Handle pagination and data fetching logic

Testing Strategy

  • Write unit tests for domain logic, repositories, and Blocs
  • Implement integration tests for features
  • Create widget tests for UI components
  • Use mocks for dependencies with mockito or mocktail
  • Follow Given-When-Then pattern for test structure
  • Aim for high test coverage of domain and data layers

Performance Considerations

  • Use const constructors for immutable widgets
  • Implement efficient list rendering with ListView.builder
  • Minimize widget rebuilds with proper state management
  • Use computation isolation for expensive operations with compute()
  • Implement pagination for large data sets
  • Cache network resources appropriately
  • Profile and optimize render performance

Code Quality

  • Use lint rules with flutter_lints package
  • Keep functions small and focused (under 30 lines)
  • Apply SOLID principles throughout the codebase
  • Use meaningful naming for classes, methods, and variables
  • Document public APIs and complex logic
  • Implement proper null safety
  • Use value objects for domain-specific types

Implementation Examples

Use Case Implementation

abstract class UseCase<Type, Params> {
  Future<Either<Failure, Type>> call(Params params);
}

class GetUser implements UseCase<User, String> {
  final UserRepository repository;

  GetUser(this.repository);

  @override
  Future<Either<Failure, User>> call(String userId) async {
    return await repository.getUser(userId);
  }
}

Repository Implementation

abstract class UserRepository {
  Future<Either<Failure, User>> getUser(String id);
  Future<Either<Failure, List<User>>> getUsers();
  Future<Either<Failure, Unit>> saveUser(User user);
}

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remoteDataSource;
  final UserLocalDataSource localDataSource;
  final NetworkInfo networkInfo;

  UserRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.networkInfo,
  });

  @override
  Future<Either<Failure, User>> getUser(String id) async {
    if (await networkInfo.isConnected) {
      try {
        final remoteUser = await remoteDataSource.getUser(id);
        await localDataSource.cacheUser(remoteUser);
        return Right(remoteUser.toDomain());
      } on ServerException {
        return Left(ServerFailure());
      }
    } else {
      try {
        final localUser = await localDataSource.getLastUser();
        return Right(localUser.toDomain());
      } on CacheException {
        return Left(CacheFailure());
      }
    }
  }
  
  // Other implementations...
}

Bloc Implementation

@freezed
class UserState with _$UserState {
  const factory UserState.initial() = _Initial;
  const factory UserState.loading() = _Loading;
  const factory UserState.loaded(User user) = _Loaded;
  const factory UserState.error(Failure failure) = _Error;
}

@freezed
class UserEvent with _$UserEvent {
  const factory UserEvent.getUser(String id) = _GetUser;
  const factory UserEvent.refreshUser() = _RefreshUser;
}

class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUser getUser;
  String? currentUserId;

  UserBloc({required this.getUser}) : super(const UserState.initial()) {
    on<_GetUser>(_onGetUser);
    on<_RefreshUser>(_onRefreshUser);
  }

  Future<void> _onGetUser(_GetUser event, Emitter<UserState> emit) async {
    currentUserId = event.id;
    emit(const UserState.loading());
    final result = await getUser(event.id);
    result.fold(
      (failure) => emit(UserState.error(failure)),
      (user) => emit(UserState.loaded(user)),
    );
  }

  Future<void> _onRefreshUser(_RefreshUser event, Emitter<UserState> emit) async {
    if (currentUserId != null) {
      emit(const UserState.loading());
      final result = await getUser(currentUserId!);
      result.fold(
        (failure) => emit(UserState.error(failure)),
        (user) => emit(UserState.loaded(user)),
      );
    }
  }
}

UI Implementation

class UserPage extends StatelessWidget {
  final String userId;

  const UserPage({Key? key, required this.userId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => getIt<UserBloc>()
        ..add(UserEvent.getUser(userId)),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('User Details'),
          actions: [
            BlocBuilder<UserBloc, UserState>(
              builder: (context, state) {
                return IconButton(
                  icon: const Icon(Icons.refresh),
                  onPressed: () {
                    context.read<UserBloc>().add(const UserEvent.refreshUser());
                  },
                );
              },
            ),
          ],
        ),
        body: BlocBuilder<UserBloc, UserState>(
          builder: (context, state) {
            return state.maybeWhen(
              initial: () => const SizedBox(),
              loading: () => const Center(child: CircularProgressIndicator()),
              loaded: (user) => UserDetailsWidget(user: user),
              error: (failure) => ErrorWidget(failure: failure),
              orElse: () => const SizedBox(),
            );
          },
        ),
      ),
    );
  }
}

Dependency Registration

final getIt = GetIt.instance;

void initDependencies() {
  // Core
  getIt.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl(getIt()));
  
  // Features - User
  // Data sources
  getIt.registerLazySingleton<UserRemoteDataSource>(
    () => UserRemoteDataSourceImpl(client: getIt()),
  );
  getIt.registerLazySingleton<UserLocalDataSource>(
    () => UserLocalDataSourceImpl(sharedPreferences: getIt()),
  );
  
  // Repository
  getIt.registerLazySingleton<UserRepository>(() => UserRepositoryImpl(
    remoteDataSource: getIt(),
    localDataSource: getIt(),
    networkInfo: getIt(),
  ));
  
  // Use cases
  getIt.registerLazySingleton(() => GetUser(getIt()));
  
  // Bloc
  getIt.registerFactory(() => UserBloc(getUser: getIt()));
}

Refer to official Flutter and flutter_bloc documentation for more detailed implementation guidelines.

Reviews (0)

Sign in to write a review.

No reviews yet. Be the first to review!

Comments (0)

Sign in to join the discussion.

No comments yet. Be the first to share your thoughts!

Related Configs