Clean Architecture in Flutter: Domain-Driven Design, BLoC & Production Patterns
A comprehensive, code-driven guide to architecting scalable Flutter applications with Clean Architecture: decoupling Domain, Data, and Presentation with BLoC, dependency inversion, and offline-first repositories.
1. The Domain Layer: Pure Dart Business Core
The Domain layer represents the core business rules of your application. It contains Entities (pure immutable data classes), Use Cases (atomic single-responsibility actions), and Repository Contracts (abstract interfaces). Because it has zero external framework dependencies, it remains completely resilient to UI redesigns, database migrations, or third-party package deprecations.
import 'package:equatable/equatable.dart';
/// Pure domain entity: zero Flutter or third-party framework dependencies.
class UserEntity extends Equatable {
final String id;
final String email;
final String displayName;
final bool isVerified;
final DateTime createdAt;
const UserEntity({
required this.id,
required this.email,
required this.displayName,
this.isVerified = false,
required this.createdAt,
});
@override
List<Object?> get props => [id, email, displayName, isVerified, createdAt];
}import '../entities/user_entity.dart';
import '../repositories/user_repository.dart';
/// Encapsulates a single atomic business action.
class GetUserProfileUseCase {
final UserRepository _repository;
const GetUserProfileUseCase(this._repository);
Future<Result<UserEntity, Failure>> execute(String userId) async {
if (userId.trim().isEmpty) {
return Error(ValidationFailure('User ID cannot be empty.'));
}
return await _repository.getUserProfile(userId);
}
}In-depth breakdown of Data, Repository, and BLoC layer decoupling by Felix Angelov and the Flutter team.
https://bloclibrary.dev/architecture/2. The Data Layer: Models, DataSources & Offline-First Strategy
The Data layer implements the repository contracts defined in the Domain layer. It coordinates Remote Data Sources (Dio, Supabase, Firebase) and Local Data Sources (Drift SQLite, Isar, SharedPreferences). Data Models extend Domain Entities to add serialization (fromJson / toJson) without polluting pure domain contracts.
import '../../domain/entities/user_entity.dart';
/// Data model extending domain entity with JSON serialization logic.
class UserModel extends UserEntity {
const UserModel({
required super.id,
required super.email,
required super.displayName,
super.isVerified,
required super.createdAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
email: json['email'] as String,
displayName: json['display_name'] as String,
isVerified: json['is_verified'] as bool? ?? false,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'email': email,
'display_name': displayName,
'is_verified': isVerified,
'created_at': createdAt.toIso8601String(),
};
}import '../../domain/entities/user_entity.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_remote_datasource.dart';
import '../datasources/user_local_datasource.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Result<UserEntity, Failure>> getUserProfile(String userId) async {
if (await networkInfo.isConnected) {
try {
final remoteUser = await remoteDataSource.fetchUser(userId);
await localDataSource.cacheUser(remoteUser);
return Success(remoteUser);
} on ServerException catch (e) {
return Error(ServerFailure(e.message));
}
} else {
// Resilient offline-first fallback
final cached = await localDataSource.getLastCachedUser(userId);
if (cached != null) return Success(cached);
return Error(NetworkFailure('Aucune connexion et aucune donnée en cache.'));
}
}
}Google's official documentation on Flutter engine architecture, reactive widget trees, and data-flow patterns.
https://docs.flutter.dev/resources/architectural-overview3. The Presentation Layer: State Management & Reactive UI
The Presentation layer contains widgets, screens, and state managers (BLoC, Cubit, or Riverpod). The UI triggers events on the BLoC, which calls the UseCase and yields immutable states (Initial, Loading, Loaded, Error). Widgets consume state with modern Dart 3 pattern matching, completely oblivious to whether the data came from Supabase or local SQLite.
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/get_user_profile_usecase.dart';
import '../../domain/entities/user_entity.dart';
// Events
sealed class UserProfileEvent {}
class LoadUserProfile extends UserProfileEvent {
final String userId;
LoadUserProfile(this.userId);
}
// States
sealed class UserProfileState {}
class UserProfileInitial extends UserProfileState {}
class UserProfileLoading extends UserProfileState {}
class UserProfileLoaded extends UserProfileState {
final UserEntity user;
UserProfileLoaded(this.user);
}
class UserProfileError extends UserProfileState {
final String message;
UserProfileError(this.message);
}
// BLoC
class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
final GetUserProfileUseCase getUserProfile;
UserProfileBloc({required this.getUserProfile}) : super(UserProfileInitial()) {
on<LoadUserProfile>((event, emit) async {
emit(UserProfileLoading());
final result = await getUserProfile.execute(event.userId);
switch (result) {
case Success(value: final user):
emit(UserProfileLoaded(user));
case Error(failure: final failure):
emit(UserProfileError(failure.message));
}
});
}
}import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/user_profile_bloc.dart';
class UserProfilePage extends StatelessWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profil')),
body: BlocBuilder<UserProfileBloc, UserProfileState>(
builder: (context, state) => switch (state) {
UserProfileInitial() => const Center(child: Text('En attente...')),
UserProfileLoading() => const Center(child: CircularProgressIndicator()),
UserProfileLoaded(:final user) => ListView(
padding: const EdgeInsets.all(24),
children: [
CircleAvatar(radius: 40, child: Text(user.displayName[0])),
const SizedBox(height: 16),
Text(user.displayName, style: Theme.of(context).textTheme.headlineSmall),
Text(user.email, style: Theme.of(context).textTheme.bodyMedium),
],
),
UserProfileError(:final message) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(message, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => context.read<UserProfileBloc>().add(LoadUserProfile(userId)),
child: const Text('Réessayer'),
),
],
),
),
},
),
);
}
}4. Dependency Injection & Unit Testing Rigor
By strictly respecting Dependency Inversion (the 'D' in SOLID), classes depend on abstract contracts rather than concrete implementations. Mocking a repository takes 3 lines with Mocktail, enabling lightning-fast unit test execution for UseCases and BLoCs without ever spinning up an emulator.
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late GetUserProfileUseCase useCase;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
useCase = GetUserProfileUseCase(mockRepository);
});
test('should return UserEntity when repository call succeeds', () async {
final tUser = UserEntity(
id: 'usr_1',
email: 'contact@alex.dev',
displayName: 'Alex Traore',
createdAt: DateTime(2026, 1, 1),
);
when(() => mockRepository.getUserProfile('usr_1'))
.thenAnswer((_) async => Success(tUser));
final result = await useCase.execute('usr_1');
expect(result, isA<Success<UserEntity, Failure>>());
verify(() => mockRepository.getUserProfile('usr_1')).called(1);
verifyNoMoreInteractions(mockRepository);
});
}Production architecture standards from Very Good Ventures, authors of Very Good CLI and leading contributors to the Flutter ecosystem.
https://verygood.ventures/blog/very-good-flutter-architecture