Implementing Clean Architecture in Flutter

Clean Architecture is a software design philosophy that separates the codebase into layers, making your app scalable, testable, and maintainable. In Flutter, applying clean architecture helps you manage complexity as your project grows.

🧩 What is Clean Architecture?

Clean Architecture divides your app into distinct layers:

  • Presentation Layer: UI and state management (e.g., widgets, BLoC, Provider)
  • Domain Layer: Business logic and entities (pure Dart, no Flutter dependencies)
  • Data Layer: Data sources (APIs, databases, repositories)

Each layer depends only on the layer below it, and the domain layer is at the core, independent of external frameworks.

🌟 Benefits of Clean Architecture

  • Separation of Concerns: Each layer has a clear responsibility.
  • Testability: Business logic can be tested independently from UI and data sources.
  • Scalability: Easy to add new features without breaking existing code.
  • Maintainability: Code is organized and easier to refactor.
  • Reusability: Domain logic can be reused across different platforms (web, mobile, etc.).

🏗️ Example Project Structure

Here’s a simple folder structure for a Flutter project using Clean Architecture:

lib/
  core/
    error/
    usecases/
    utils/
  features/
    todo/
      data/
        datasources/
        models/
        repositories/
      domain/
        entities/
        repositories/
        usecases/
      presentation/
        bloc/
        pages/
        widgets/

🚀 Brief Example

Suppose you’re building a ToDo app. Here’s how you might structure the “Add Task” feature:

Domain Layer (entities, usecases)

// lib/features/todo/domain/entities/task.dart
class Task {
  final String id;
  final String title;
  final bool isCompleted;

  Task({required this.id, required this.title, this.isCompleted = false});
}

// lib/features/todo/domain/usecases/add_task.dart
class AddTask {
  final TaskRepository repository;
  AddTask(this.repository);

  Future<void> call(Task task) async {
    await repository.addTask(task);
  }
}

Data Layer (repositories, datasources)

// lib/features/todo/data/repositories/task_repository_impl.dart
class TaskRepositoryImpl implements TaskRepository {
  final TaskLocalDataSource localDataSource;
  TaskRepositoryImpl(this.localDataSource);

  @override
  Future<void> addTask(Task task) async {
    await localDataSource.addTask(task);
  }
}

Presentation Layer (bloc, widgets)

// lib/features/todo/presentation/bloc/task_bloc.dart
class TaskBloc extends Bloc<TaskEvent, TaskState> {
  final AddTask addTask;
  TaskBloc(this.addTask) : super(TaskInitial());

  @override
  Stream<TaskState> mapEventToState(TaskEvent event) async* {
    if (event is AddTaskEvent) {
      await addTask(event.task);
      yield TaskAdded();
    }
  }
}

📦 Template

You can use the above folder structure as a template for your next Flutter project. Create separate folders for each feature and layer, and keep your business logic independent from UI and data sources.

📚 Resources

Happy coding! 🚀