Clean Architecture in Flutter – A Complete Guide With Code Examples (2025 Edition)
November 14, 2025
Building scalable Flutter apps becomes challenging as features grow, teams expand, and codebases get complicated. This is where Clean Architecture shines — a proven architecture pattern that keeps your app testable, maintainable, scalable, and independent of frameworks.
In this blog, we’ll break down Clean Architecture for Flutter in simple terms with folder structure, data flow, and real code examples.
What is Clean Architecture?
Proposed by Robert C. Martin (“Uncle Bob”), Clean Architecture divides an app into independent layers, each responsible for a specific job.
The goals are:
- Independence → UI should not depend on data sources
- Separation of concerns
- Easy to test
- Easy to scale or replace components
- Business logic stays pure
Clean Architecture Layers in Flutter
Flutter projects commonly follow 3 layers:
lib/
└── src/
├── presentation/ → UI (Widgets, Screens, Controllers)
├── domain/ → Business logic (Entities, Repositories, Use Cases)
└── data/ → API, DB, models, repository implementations
1. Domain Layer (Core Business Logic)
This is the heart of your application.
It contains:
✔ Entities
Plain Dart classes that represent business objects.
✔ Use Cases
Business rules. Each use case does only one thing.
✔ Abstract Repository
Defines what data operations are possible.
Example: Domain Layer
entity – user.dart
class User {
final int id;
final String name;
User({required this.id, required this.name});
}
repository – user_repository.dart
abstract class UserRepository {
Future<User> getUserById(int id);
}
use case – get_user.dart
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future<User> call(int id) async {
return await repository.getUserById(id);
}
}
The domain layer contains zero Flutter dependencies.
It’s pure Dart → easy to test, reusable, and independent.
2. Data Layer (API + DB + Models)
The data layer communicates with:
- REST API
- Local database (Hive/SQLite/Isar)
- Third-party services
And it implements the domain repository using:
✔ Models (converts JSON to Dart objects)
✔ Remote Data Source
✔ Local Data Source
✔ Repository Implementation
Example: Data Layer
model – user_model.dart
import '../../domain/entities/user.dart';
class UserModel extends User {
UserModel({required super.id, required super.name});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json["id"],
name: json["name"],
);
}
}
remote data source – user_remote_data_source.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/user_model.dart';
class UserRemoteDataSource {
final http.Client client;
UserRemoteDataSource(this.client);
Future<UserModel> fetchUser(int id) async {
final response = await client.get(Uri.parse("https://reqres.in/api/users/$id"));
final data = jsonDecode(response.body);
return UserModel.fromJson(data['data']);
}
}
repository_impl – user_repository_impl.dart
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_remote_data_source.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
UserRepositoryImpl(this.remoteDataSource);
@override
Future<User> getUserById(int id) {
return remoteDataSource.fetchUser(id);
}
}
3. Presentation Layer (UI + State Management)
This includes:
- Widgets
- Screens
- Providers/Bloc/Riverpod controllers
The UI layer depends on use cases, not the data layer.
Example: Presentation Layer (Using Riverpod)
user_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/user.dart';
import '../../domain/usecases/get_user.dart';
class UserNotifier extends StateNotifier<AsyncValue<User>> {
final GetUser getUser;
UserNotifier(this.getUser) : super(const AsyncLoading());
Future<void> loadUser(int id) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => getUser(id));
}
}
providers.dart
final userRepositoryProvider = Provider(
(ref) => UserRepositoryImpl(UserRemoteDataSource(http.Client())),
);
final getUserProvider = Provider(
(ref) => GetUser(ref.read(userRepositoryProvider)),
);
final userNotifierProvider =
StateNotifierProvider<UserNotifier, AsyncValue<User>>(
(ref) => UserNotifier(ref.read(getUserProvider)),
);
UI – user_page.dart
class UserPage extends ConsumerWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text("User Details")),
body: userState.when(
data: (user) => Center(
child: Text("Hello, ${user.name}"),
),
error: (e, _) => Center(child: Text("Error: $e")),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
}
How the Data Flows (Simple Diagram)
UI (Presentation)
|
▼
Use Case (Domain)
|
▼
Repository Interface (Domain)
|
▼
Repository Impl (Data)
|
▼
Remote / Local Datasource (Data)
Each layer communicates only with the layer below it.
Why Clean Architecture Works Best for Large Flutter Apps
✔ Modular & Scalable
Easy to add new features without breaking existing ones.
✔ Testable
Domain logic is pure Dart → 100% unit test coverage is achievable.
✔ Team Friendly
Different teams can work on domain, data, UI independently.
✔ Replaceable
Want to switch from REST → GraphQL? Only update the data layer.
Best Practices for Clean Architecture in Flutter
- Use DI (GetIt / Riverpod)
- Maintain 1 use case = 1 responsibility
- Keep domain layer pure
- Avoid putting business logic in UI
- Keep widgets dumb, view models smart
- Add folder-by-feature for large apps
- Use Freezed for models & entities
Conclusion
Clean Architecture helps you build robust, scalable, testable Flutter apps suitable for enterprise-level products. With proper layering and separation of concerns, your code becomes future-proof and easier to extend.
If you’re building a long-term project or working in a team, Clean Architecture is absolutely worth implementing.