Clean Architecture Implementation in Flutter
A quick guide on how to implement Clean Architecture in Flutter
Introduction
Clean Architecture is a software design philosophy that emphasizes separation of concerns and maintainability by dividing the software into layers with distinct responsibilities. In the context of Flutter, Clean Architecture can be a powerful approach to create scalable, testable, and maintainable applications.
This technical writing will guide you through the implementation of Clean Architecture in a Flutter project, covering key concepts, layers, and code examples.
Key Concepts of Clean Architecture
Clean Architecture comprises the following layers:
Entities: Represent the core business logic and contain business rules and data structures.
Use Cases (Interactors): Define application-specific business rules and orchestrate the flow of data between the entities and the outer layers.
Interface Adapters: Convert data between the use cases and the external systems, such as the UI, databases, or external services. Includes presenters, controllers, and gateways.
Frameworks and Drivers: External frameworks, tools, and libraries, including the UI framework (Flutter), databases, and external services.
The primary goal is to keep the inner layers independent of the outer layers, allowing for easier maintenance, testing, and future changes.
Project Structure
Let's create a simple Flutter project with a Clean Architecture structure. Consider a basic task management app.
project_root/
|-- lib/
| |-- core/
| | |-- entities/
| | |-- use_cases/
| | | |-- task_use_case.dart
| | |-- repositories/
| | |-- task_repository.dart
| |
| |-- data/
| | |-- repositories_impl/
| | | |-- task_repository_impl.dart
| |
| |-- presentation/
| | |-- screens/
| | | |-- task_list_screen.dart
| | | |-- add_task_screen.dart
| | |-- presenters/
| | |-- task_list_presenter.dart
| | |-- add_task_presenter.dart
| |
| |-- app.dart
|-- main.dart
Entities
// lib/core/entities/task.dart
class Task {
final String id;
final String title;
final bool isCompleted;
Task({required this.id, required this.title, required this.isCompleted});
}
Use Cases
// lib/core/use_cases/task_use_case.dart
import 'package:your_project/core/entities/task.dart';
abstract class TaskUseCase {
Future<List<Task>> getTasks();
Future<void> addTask(Task task);
}
Repositories
// lib/core/repositories/task_repository.dart
import 'package:your_project/core/entities/task.dart';
abstract class TaskRepository {
Future<List<Task>> getTasks();
Future<void> addTask(Task task);
}
Repository Implementation
// lib/data/repositories_impl/task_repository_impl.dart
import 'package:your_project/core/entities/task.dart';
import 'package:your_project/core/repositories/task_repository.dart';
class TaskRepositoryImpl implements TaskRepository {
@override
Future<List<Task>> getTasks() async {
// Implementation to fetch tasks from a data source (e.g., database)
// ...
}
@override
Future<void> addTask(Task task) async {
// Implementation to add a task to a data source (e.g., database)
// ...
}
}
Presenters
// lib/presentation/presenters/task_list_presenter.dart
import 'package:your_project/core/entities/task.dart';
import 'package:your_project/core/use_cases/task_use_case.dart';
class TaskListPresenter {
final TaskUseCase taskUseCase;
TaskListPresenter(this.taskUseCase);
Future<List<Task>> getTasks() async {
return await taskUseCase.getTasks();
}
}
UI Screens
// lib/presentation/screens/task_list_screen.dart
import 'package:flutter/material.dart';
import 'package:your_project/presentation/presenters/task_list_presenter.dart';
class TaskListScreen extends StatefulWidget {
final TaskListPresenter presenter;
TaskListScreen({required this.presenter});
@override
_TaskListScreenState createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Task List')),
body: FutureBuilder(
future: widget.presenter.getTasks(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
List<Task> tasks = snapshot.data as List<Task>;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(tasks[index].title),
// ... other task details
);
},
);
}
},
),
);
}
}
Wiring It All Together
// lib/app.dart
import 'package:flutter/material.dart';
import 'package:your_project/data/repositories_impl/task_repository_impl.dart';
import 'package:your_project/presentation/presenters/task_list_presenter.dart';
import 'package:your_project/presentation/screens/task_list_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Clean Architecture Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TaskListScreen(
presenter: TaskListPresenter(
TaskRepositoryImpl(),
),
),
);
}
}
Conclusion
This example provides a foundation for implementing Clean Architecture in a Flutter project. The separation of concerns allows for easier testing and maintenance, and the codebase can be extended with new features without affecting existing components. It's crucial to adapt the structure based on the project's specific requirements and complexities.