Clean Architecture in Flutter: A Practical Guide
How I structure production Flutter apps using Clean Architecture — separating domain, data, and presentation layers with real code examples.
Series: Flutter Architecture Patterns
Part 1
Why Architecture Matters in Flutter
Flutter lets you ship fast — sometimes too fast. Without intentional structure, widgets balloon into 500-line behemoths that own networking, business logic, and rendering all at once.
Clean Architecture draws three concentric layers:
- Domain — pure Dart: entities, use cases, repository interfaces
- Data — implements domain interfaces: remote sources, local cache, DTOs
- Presentation — Flutter widgets + state management (Riverpod / Bloc)
The rule: dependencies point inward. Domain knows nothing about Flutter or Firebase.

Folder Structure
lib/
├── core/ # Shared utilities, error types, network client
├── features/
│ └── projects/
│ ├── domain/
│ │ ├── entities/project.dart
│ │ ├── repositories/project_repository.dart
│ │ └── usecases/get_projects.dart
│ ├── data/
│ │ ├── datasources/projects_remote_source.dart
│ │ ├── models/project_model.dart
│ │ └── repositories/project_repository_impl.dart
│ └── presentation/
│ ├── pages/projects_page.dart
│ ├── widgets/project_card.dart
│ └── providers/projects_provider.dart
└── injection_container.dartThe Domain Layer
// entities/project.dart — plain Dart, zero Flutter imports
class Project {
final String id;
final String title;
final String description;
final List<String> stack;
const Project({
required this.id,
required this.title,
required this.description,
required this.stack,
});
}
// repositories/project_repository.dart — interface only
abstract class ProjectRepository {
Future<List<Project>> getProjects();
}
// usecases/get_projects.dart
class GetProjects {
final ProjectRepository _repo;
GetProjects(this._repo);
Future<List<Project>> call() => _repo.getProjects();
}The use case is a single callable class — easy to test in pure Dart without any Flutter test machinery.
Testing the Use Case
void main() {
late MockProjectRepository mockRepo;
late GetProjects useCase;
setUp(() {
mockRepo = MockProjectRepository();
useCase = GetProjects(mockRepo);
});
test('returns projects from repository', () async {
when(() => mockRepo.getProjects()).thenAnswer((_) async => fakeProjects);
final result = await useCase();
expect(result, fakeProjects);
});
}No pumpWidget, no tester — pure business-logic test that runs in milliseconds.
Key Takeaways
- Keep
domain/free of Flutter and third-party imports - Use cases = one public method, testable in isolation
- Let DI wire everything together (
get_itor Riverpod providers) - The data layer absorbs all API/DB churn so domain and UI stay stable