Clean Architecture with Four Layers for Flutter Apps
Implement Clean Architecture in Flutter with four distinct layers: Domain, Data, Presentation, and Application. Scale your Flutter apps without the spaghetti code.
Published on • September 15, 2026
AI Assistant

Clean Architecture in Flutter isn’t just about separating files — it’s about creating boundaries that let you change implementations without breaking consumers. The four-layer variant (Domain, Data, Presentation, Application) gives each concern its own rules and dependencies.
The four layers
┌─────────────────────────────────┐
│ Application Layer │ ← Coordinates repositories, cross-cutting concerns
├─────────────────────────────────┤
│ Presentation Layer │ ← Widgets + controllers/state management
├─────────────────────────────────┤
│ Domain Layer │ ← Entities + use cases (pure Dart)
├─────────────────────────────────┤
│ Data Layer │ ← Repositories + data sources (API, DB, cache)
└─────────────────────────────────┘
Dependency rule: Each layer only depends on layers below it. Domain knows nothing about Data or Presentation.
Domain layer (entities + use cases)
The Domain layer contains business logic with zero Flutter dependencies:
// entities/user.dart
class User {
final String id;
final String name;
final String email;
const User({required this.id, required this.name, required this.email});
}
// use_cases/get_user.dart
class GetUser {
final UserRepository _repository;
GetUser(this._repository);
Future<User> call(String id) async {
if (id.isEmpty) throw InvalidArgumentException('User ID required');
return await _repository.getUserById(id);
}
}
Use cases are functions wrapped in classes. They orchestrate domain logic without knowing where data comes from.
Data layer (repositories + data sources)
The Data layer implements domain interfaces and handles external data:
// data/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource _remote;
final UserLocalDataSource _local;
UserRepositoryImpl(this._remote, this._local);
@override
Future<User> getUserById(String id) async {
try {
final user = await _remote.fetchUser(id);
await _local.cacheUser(user);
return user;
} catch (e) {
return await _local.getCachedUser(id);
}
}
}
Data sources are abstracted behind interfaces, making testing trivial with mocks.
Presentation layer (widgets + controllers)
The Presentation layer handles UI and state management:
// presentation/providers/user_provider.dart
class UserNotifier extends StateNotifier<AsyncValue<User>> {
final GetUser _getUser;
UserNotifier(this._getUser) : super(const AsyncValue.loading());
Future<void> loadUser(String id) async {
state = const AsyncValue.loading();
try {
final user = await _getUser(id);
state = AsyncValue.data(user);
} catch (e) {
state = AsyncValue.error(e);
}
}
}
// presentation/widgets/user_card.dart
class UserCard extends ConsumerWidget {
final String userId;
const UserCard({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (user) => Card(child: Text(user.name)),
);
}
}
Application layer (optional coordinator)
The Application layer coordinates multiple repositories for complex workflows:
// application/use_cases/register_user.dart
class RegisterUser {
final UserRepository _userRepo;
final AuthRepository _authRepo;
final EmailService _emailService;
RegisterUser(this._userRepo, this._authRepo, this._emailService);
Future<User> call(RegisterUserCommand command) async {
final user = await _userRepo.createUser(command.toEntity());
await _authRepo.set_password(user.id, command.password);
await _emailService.sendWelcome(user.email);
return user;
}
}
This layer prevents use cases from knowing about each other while coordinating cross-cutting concerns.
Feature-based folder structure
Organize by feature, not by layer type:
lib/
├── core/ # Shared utilities, theme, constants
│ ├── theme/
│ └── utils/
├── features/
│ ├── auth/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── use_cases/
│ │ ├── data/
│ │ │ ├── data_sources/
│ │ │ └── repositories/
│ │ └── presentation/
│ │ ├── providers/
│ │ └── widgets/
│ ├── dashboard/
│ │ ├── domain/
│ │ ├── data/
│ │ └── presentation/
│ └── settings/
│ ├── domain/
│ ├── data/
│ └── presentation/
└── main.dart
Each feature is self-contained. You can delete a feature without breaking others.
Dependency injection
Wire everything together with dependency injection:
// injection.dart
final getIt = GetIt.instance;
void setupDependencies() {
// Data layer
getIt.registerLazySingleton<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(getIt()),
);
getIt.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(getIt(), getIt()),
);
// Domain layer
getIt.registerLazySingleton(() => GetUser(getIt()));
// Presentation layer
getIt.registerFactory(() => UserNotifier(getIt()));
}
Benefits of four-layer Clean Architecture
- Testability: Mock any layer boundary
- Replaceability: Swap SQLite for Hive without touching Presentation
- Scalability: Features don’t interfere with each other
- Team productivity: Different developers own different layers
- Business logic isolation: Domain layer is pure Dart, testable without Flutter
Clean Architecture isn’t overkill for small apps, but it pays for itself the moment your app grows beyond a few screens.