Clean Architecture
Answer
Overview
Clean Architecture is an architectural pattern that separates code into distinct layers, each with a specific responsibility. The goal is to create maintainable, testable, and scalable applications by keeping business logic independent from UI, frameworks, and external dependencies.
Originally proposed by Robert C. Martin (Uncle Bob), Clean Architecture ensures that:
- Business logic is independent of UI and frameworks
- Code is easy to test
- Changes in one layer don't affect others
Core Principles
- Separation of Concerns - Each layer has a single responsibility
- Dependency Rule - Dependencies point inward (toward business logic)
- Framework Independence - Business logic doesn't depend on Flutter
- Testability - Each layer can be tested independently
- UI Independence - Business logic doesn't know about UI
Layers in Clean Architecture
text┌─────────────────────────────────────────┐ │ Presentation Layer │ ← UI, Widgets, State Management ├─────────────────────────────────────────┤ │ Domain Layer │ ← Business Logic, Use Cases ├─────────────────────────────────────────┤ │ Data Layer │ ← Repositories, Data Sources └─────────────────────────────────────────┘
1. Presentation Layer (UI)
Responsibility: Display UI and handle user interactions
Contains:
- Widgets (Screens, Components)
- State Management (BLoC, Riverpod, Provider)
- ViewModels/Presenters
dart// presentation/pages/user_page.dart class UserPage extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder<UserBloc, UserState>( builder: (context, state) { if (state is UserLoading) { return CircularProgressIndicator(); } else if (state is UserLoaded) { return ListView.builder( itemCount: state.users.length, itemBuilder: (context, index) { return ListTile( title: Text(state.users[index].name), ); }, ); } else if (state is UserError) { return Text('Error: ${state.message}'); } return Container(); }, ); } } // presentation/bloc/user_bloc.dart class UserBloc extends Bloc<UserEvent, UserState> { final GetUsers getUsers; UserBloc({required this.getUsers}) : super(UserInitial()) { on<LoadUsers>((event, emit) async { emit(UserLoading()); final result = await getUsers(NoParams()); result.fold( (failure) => emit(UserError(message: failure.message)), (users) => emit(UserLoaded(users: users)), ); }); } }
2. Domain Layer (Business Logic)
Responsibility: Contains the business rules and use cases
Contains:
- Entities (Business objects)
- Use Cases (Business operations)
- Repository Interfaces (abstractions)
dart// domain/entities/user.dart class User { final int id; final String name; final String email; User({required this.id, required this.name, required this.email}); } // domain/repositories/user_repository.dart abstract class UserRepository { Future<Either<Failure, List<User>>> getUsers(); Future<Either<Failure, User>> getUserById(int id); } // domain/usecases/get_users.dart class GetUsers { final UserRepository repository; GetUsers(this.repository); Future<Either<Failure, List<User>>> call(NoParams params) async { return await repository.getUsers(); } }
3. Data Layer
Responsibility: Handle data from external sources (API, database, cache)
Contains:
- Repository Implementations
- Data Sources (Remote, Local)
- Models (Data transfer objects)
dart// data/models/user_model.dart class UserModel extends User { UserModel({required int id, required String name, required String email}) : super(id: id, name: name, email: email); factory UserModel.fromJson(Map<String, dynamic> json) { return UserModel( id: json['id'], name: json['name'], email: json['email'], ); } Map<String, dynamic> toJson() { return {'id': id, 'name': name, 'email': email}; } } // data/datasources/user_remote_datasource.dart abstract class UserRemoteDataSource { Future<List<UserModel>> getUsers(); } class UserRemoteDataSourceImpl implements UserRemoteDataSource { final http.Client client; UserRemoteDataSourceImpl({required this.client}); Future<List<UserModel>> getUsers() async { final response = await client.get( Uri.parse('https://api.example.com/users'), ); if (response.statusCode == 200) { final List<dynamic> jsonList = json.decode(response.body); return jsonList.map((json) => UserModel.fromJson(json)).toList(); } else { throw ServerException(); } } } // data/repositories/user_repository_impl.dart class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; UserRepositoryImpl({ required this.remoteDataSource, required this.localDataSource, }); Future<Either<Failure, List<User>>> getUsers() async { try { final remoteUsers = await remoteDataSource.getUsers(); localDataSource.cacheUsers(remoteUsers); return Right(remoteUsers); } on ServerException { return Left(ServerFailure()); } on CacheException { return Left(CacheFailure()); } } }
Folder Structure
textlib/ ├── core/ │ ├── error/ │ │ ├── failures.dart │ │ └── exceptions.dart │ ├── usecases/ │ │ └── usecase.dart │ └── utils/ │ └── constants.dart ├── features/ │ └── user/ │ ├── data/ │ │ ├── datasources/ │ │ │ ├── user_remote_datasource.dart │ │ │ └── user_local_datasource.dart │ │ ├── models/ │ │ │ └── user_model.dart │ │ └── repositories/ │ │ └── user_repository_impl.dart │ ├── domain/ │ │ ├── entities/ │ │ │ └── user.dart │ │ ├── repositories/ │ │ │ └── user_repository.dart │ │ └── usecases/ │ │ ├── get_users.dart │ │ └── get_user_by_id.dart │ └── presentation/ │ ├── bloc/ │ │ ├── user_bloc.dart │ │ ├── user_event.dart │ │ └── user_state.dart │ ├── pages/ │ │ └── user_page.dart │ └── widgets/ │ └── user_card.dart └── main.dart
Dependency Injection
Use get_it for dependency injection.
dart// injection_container.dart final sl = GetIt.instance; Future<void> init() async { // BLoC sl.registerFactory(() => UserBloc(getUsers: sl())); // Use Cases sl.registerLazySingleton(() => GetUsers(sl())); // Repository sl.registerLazySingleton<UserRepository>( () => UserRepositoryImpl( remoteDataSource: sl(), localDataSource: sl(), ), ); // Data Sources sl.registerLazySingleton<UserRemoteDataSource>( () => UserRemoteDataSourceImpl(client: sl()), ); sl.registerLazySingleton<UserLocalDataSource>( () => UserLocalDataSourceImpl(sharedPreferences: sl()), ); // External final sharedPreferences = await SharedPreferences.getInstance(); sl.registerLazySingleton(() => sharedPreferences); sl.registerLazySingleton(() => http.Client()); } // main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); await init(); runApp(MyApp()); }
Benefits
| Benefit | Description |
|---|---|
| Testability | Each layer can be tested independently |
| Maintainability | Changes in UI don't affect business logic |
| Scalability | Easy to add new features without breaking existing code |
| Flexibility | Easy to switch frameworks, databases, or APIs |
| Separation | Clear boundaries between layers |
Clean Architecture vs Other Patterns
| Pattern | Layers | Complexity | Use Case |
|---|---|---|---|
| Clean Architecture | 3+ layers | High | Large, complex apps |
| MVVM | 3 layers | Medium | Medium apps |
| MVC | 3 layers | Low | Simple apps |
| MVP | 3 layers | Medium | Android-style apps |
Common Packages
yamldependencies: # State Management flutter_bloc: ^8.1.3 # Dependency Injection get_it: ^7.6.0 # Functional Programming (Either) dartz: ^0.10.1 # HTTP http: ^1.1.0 # Local Storage shared_preferences: ^2.2.0 # Code Generation freezed: ^2.4.5 json_serializable: ^6.7.1
Summary
Clean Architecture separates code into Presentation (UI), Domain (Business Logic), and Data (External Sources) layers. This ensures testability, maintainability, and scalability by keeping business logic independent of frameworks and UI.
Learn more at Clean Architecture by Uncle Bob and Flutter Clean Architecture Guide.