Question #360HardArchitectureImportant

Clean Architecture

#architecture#clean-architecture#design-patterns

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

  1. Separation of Concerns - Each layer has a single responsibility
  2. Dependency Rule - Dependencies point inward (toward business logic)
  3. Framework Independence - Business logic doesn't depend on Flutter
  4. Testability - Each layer can be tested independently
  5. 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

text
lib/
├── 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

BenefitDescription
TestabilityEach layer can be tested independently
MaintainabilityChanges in UI don't affect business logic
ScalabilityEasy to add new features without breaking existing code
FlexibilityEasy to switch frameworks, databases, or APIs
SeparationClear boundaries between layers

Clean Architecture vs Other Patterns

PatternLayersComplexityUse Case
Clean Architecture3+ layersHighLarge, complex apps
MVVM3 layersMediumMedium apps
MVC3 layersLowSimple apps
MVP3 layersMediumAndroid-style apps

Common Packages

yaml
dependencies:
  # 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.