
Gad Ntenta
Développeur Full Stack
Architecture des Applications Flutter
FlutterArchitectureClean ArchitectureState Management
Découvrez les meilleures pratiques d'architecture pour vos applications Flutter.
Architecture des Applications Flutter
Introduction
Une bonne architecture est essentielle pour créer des applications Flutter maintenables et évolutives. Dans cet article, nous allons explorer les différentes approches d'architecture et les meilleures pratiques.
Clean Architecture
1. Structure des Couches
// Core Layer
abstract class UseCase<Type, Params> {
Future<Type> call(Params params);
}
// Domain Layer
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> saveUser(User user);
}
// Data Layer
class UserRepositoryImpl implements UserRepository {
final ApiClient apiClient;
final LocalStorage localStorage;
UserRepositoryImpl(this.apiClient, this.localStorage);
@override
Future<User> getUser(String id) async {
try {
final user = await apiClient.getUser(id);
await localStorage.saveUser(user);
return user;
} catch (e) {
return localStorage.getUser(id);
}
}
@override
Future<void> saveUser(User user) async {
await apiClient.saveUser(user);
await localStorage.saveUser(user);
}
}
// Presentation Layer
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
final SaveUserUseCase saveUserUseCase;
UserBloc(this.getUserUseCase, this.saveUserUseCase) : super(UserInitial()) {
on<GetUserEvent>(_onGetUser);
on<SaveUserEvent>(_onSaveUser);
}
Future<void> _onGetUser(GetUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final user = await getUserUseCase(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onSaveUser(SaveUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
await saveUserUseCase(event.user);
emit(UserSaved());
} catch (e) {
emit(UserError(e.toString()));
}
}
}
2. Injection de Dépendances
class DependencyInjection {
static final GetIt getIt = GetIt.instance;
static void init() {
// Core
getIt.registerLazySingleton(() => ApiClient());
getIt.registerLazySingleton(() => LocalStorage());
// Repositories
getIt.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(getIt(), getIt()),
);
// Use Cases
getIt.registerLazySingleton(() => GetUserUseCase(getIt()));
getIt.registerLazySingleton(() => SaveUserUseCase(getIt()));
// BLoCs
getIt.registerFactory(() => UserBloc(getIt(), getIt()));
}
}
Feature-First Architecture
1. Structure des Modules
// Auth Module
class AuthModule extends Module {
@override
List<Bind> get binds => [
Bind.singleton((i) => AuthRepository(i())),
Bind.singleton((i) => LoginUseCase(i())),
Bind.singleton((i) => RegisterUseCase(i())),
Bind.factory((i) => AuthBloc(i(), i())),
];
@override
List<ModularRoute> get routes => [
ChildRoute('/login', child: (_, __) => LoginPage()),
ChildRoute('/register', child: (_, __) => RegisterPage()),
];
}
// Home Module
class HomeModule extends Module {
@override
List<Bind> get binds => [
Bind.singleton((i) => HomeRepository(i())),
Bind.singleton((i) => GetPostsUseCase(i())),
Bind.factory((i) => HomeBloc(i())),
];
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (_, __) => HomePage()),
ChildRoute('/post/:id', child: (_, __) => PostDetailPage()),
];
}
2. Navigation Modulaire
class AppModule extends Module {
@override
List<ModularRoute> get routes => [
ModuleRoute('/auth', module: AuthModule()),
ModuleRoute('/home', module: HomeModule()),
];
}
class AppWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: ModularRouteInformationParser(),
routerDelegate: ModularRouterDelegate(),
);
}
}
State Management
1. BLoC Pattern
// Events
abstract class UserEvent {}
class GetUserEvent extends UserEvent {
final String userId;
GetUserEvent(this.userId);
}
class SaveUserEvent extends UserEvent {
final User user;
SaveUserEvent(this.user);
}
// States
abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}
// BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
final SaveUserUseCase saveUserUseCase;
UserBloc(this.getUserUseCase, this.saveUserUseCase) : super(UserInitial()) {
on<GetUserEvent>(_onGetUser);
on<SaveUserEvent>(_onSaveUser);
}
Future<void> _onGetUser(GetUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final user = await getUserUseCase(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onSaveUser(SaveUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
await saveUserUseCase(event.user);
emit(UserSaved());
} catch (e) {
emit(UserError(e.toString()));
}
}
}
2. Provider Pattern
class UserProvider extends ChangeNotifier {
final GetUserUseCase getUserUseCase;
final SaveUserUseCase saveUserUseCase;
UserProvider(this.getUserUseCase, this.saveUserUseCase);
User? _user;
bool _isLoading = false;
String? _error;
User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> getUser(String id) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_user = await getUserUseCase(id);
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> saveUser(User user) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
await saveUserUseCase(user);
_user = user;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
Navigation
1. Gestion des Routes
class AppRouter {
static const String home = '/';
static const String login = '/login';
static const String register = '/register';
static const String profile = '/profile';
static const String settings = '/settings';
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case home:
return MaterialPageRoute(builder: (_) => HomePage());
case login:
return MaterialPageRoute(builder: (_) => LoginPage());
case register:
return MaterialPageRoute(builder: (_) => RegisterPage());
case profile:
return MaterialPageRoute(builder: (_) => ProfilePage());
case settings:
return MaterialPageRoute(builder: (_) => SettingsPage());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('Route not found!'),
),
),
);
}
}
}
2. Navigation avec Paramètres
class NavigationService {
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static Future<dynamic> navigateTo(String routeName, {Object? arguments}) {
return navigatorKey.currentState!.pushNamed(routeName, arguments: arguments);
}
static Future<dynamic> navigateToReplacement(String routeName, {Object? arguments}) {
return navigatorKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
}
static Future<dynamic> navigateToAndClear(String routeName, {Object? arguments}) {
return navigatorKey.currentState!.pushNamedAndRemoveUntil(
routeName,
(Route<dynamic> route) => false,
arguments: arguments,
);
}
static void goBack() {
return navigatorKey.currentState!.pop();
}
}
// Utilisation
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userId = ModalRoute.of(context)!.settings.arguments as String;
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => NavigationService.goBack(),
),
),
body: UserProfileContent(userId: userId),
);
}
}
Meilleures Pratiques
1. Organisation du Code
- Séparer les responsabilités
- Utiliser des modules
- Suivre les principes SOLID
- Maintenir la cohérence
2. Gestion des Erreurs
- Implémenter une gestion d'erreurs centralisée
- Utiliser des types d'erreurs personnalisés
- Logger les erreurs
- Gérer les erreurs de réseau
3. Tests
- Écrire des tests unitaires
- Écrire des tests d'intégration
- Écrire des tests de widget
- Automatiser les tests
Conclusion
Une bonne architecture est la clé du succès d'une application Flutter. En suivant ces principes et en utilisant les bonnes pratiques, vous pouvez créer des applications maintenables, évolutives et de haute qualité.
N'oubliez pas de :
- Choisir l'architecture adaptée à votre projet
- Maintenir la cohérence du code
- Documenter votre architecture
- Évoluer avec votre application
Avec ces connaissances, vous êtes prêt à créer des applications Flutter bien structurées !