Patterns avancés de gestion d'état
Architecture recommandée
Section titled “Architecture recommandée”Pour une architecture robuste et maintenable, il est essentiel de séparer les responsabilités :
Séparation des couches
Section titled “Séparation des couches”-
Présentation : Les widgets d’interface utilisateur
- Widgets, pages, composants UI
- Gestion des interactions utilisateur
- Affichage des données
-
Logique métier : Les contrôleurs et services
- Business logic, règles métier
- Gestion d’état applicatif
- Coordination des opérations
-
Données : Les modèles et repositories
- Accès aux données (API, base de données)
- Modèles de données
- Cache et persistance
Exemple d’architecture en couches
Section titled “Exemple d’architecture en couches”// Couche Donnéesclass UserRepository { final ApiService _apiService; final LocalStorage _localStorage;
UserRepository(this._apiService, this._localStorage);
Future<User> getUser(String id) async { try { return await _apiService.getUser(id); } catch (e) { return await _localStorage.getUser(id); } }}
// Couche Logique métierclass UserController extends ChangeNotifier { final UserRepository _repository;
UserController(this._repository);
User? _currentUser; User? get currentUser => _currentUser;
Future<void> loadUser(String id) async { _currentUser = await _repository.getUser(id); notifyListeners(); }}
// Couche Présentationclass UserProfileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer<UserController>( builder: (context, controller, child) { final user = controller.currentUser; if (user == null) return CircularProgressIndicator();
return Column( children: [ Text(user.name), Text(user.email), ], ); }, ); }}Patterns populaires
Section titled “Patterns populaires”Observer Pattern
Section titled “Observer Pattern”Utilisé par ValueNotifier et ChangeNotifier pour implémenter la réactivité :
abstract class Observable<T> { void addListener(VoidCallback listener); void removeListener(VoidCallback listener); void notifyListeners();}
class CustomNotifier<T> extends Observable<T> { T _value; T get value => _value;
final List<VoidCallback> _listeners = [];
CustomNotifier(this._value);
set value(T newValue) { if (_value != newValue) { _value = newValue; notifyListeners(); } }
@override void addListener(VoidCallback listener) { _listeners.add(listener); }
@override void removeListener(VoidCallback listener) { _listeners.remove(listener); }
@override void notifyListeners() { for (final listener in _listeners) { listener(); } }}Repository Pattern
Section titled “Repository Pattern”Pour abstraire l’accès aux données et faciliter les tests :
abstract class TaskRepository { Future<List<Task>> getTasks(); Future<Task> createTask(CreateTaskRequest request); Future<Task> updateTask(String id, UpdateTaskRequest request); Future<void> deleteTask(String id);}
class ApiTaskRepository implements TaskRepository { final ApiService _apiService;
ApiTaskRepository(this._apiService);
@override Future<List<Task>> getTasks() async { final response = await _apiService.get('/tasks'); return response.data.map((json) => Task.fromJson(json)).toList(); }
@override Future<Task> createTask(CreateTaskRequest request) async { final response = await _apiService.post('/tasks', request.toJson()); return Task.fromJson(response.data); }
// ... autres méthodes}
class MockTaskRepository implements TaskRepository { final List<Task> _tasks = [];
@override Future<List<Task>> getTasks() async { return List.from(_tasks); }
@override Future<Task> createTask(CreateTaskRequest request) async { final task = Task( id: DateTime.now().millisecondsSinceEpoch.toString(), title: request.title, description: request.description, isCompleted: false, ); _tasks.add(task); return task; }
// ... autres méthodes}Service Locator
Section titled “Service Locator”Via les InheritedWidget et Provider pour l’injection de dépendances :
class ServiceLocator extends InheritedWidget { final Map<Type, dynamic> _services = {};
ServiceLocator({required Widget child}) : super(child: child);
void register<T>(T service) { _services[T] = service; }
T get<T>() { final service = _services[T]; if (service == null) { throw Exception('Service ${T.toString()} not registered'); } return service as T; }
static ServiceLocator of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ServiceLocator>()!; }
@override bool updateShouldNotify(ServiceLocator oldWidget) => false;}MVVM (Model-View-ViewModel)
Section titled “MVVM (Model-View-ViewModel)”Pattern avec ChangeNotifier pour séparer la logique de présentation :
// Modelclass User { final String id; final String name; final String email;
User({required this.id, required this.name, required this.email});}
// ViewModelclass UserProfileViewModel extends ChangeNotifier { final UserRepository _repository;
UserProfileViewModel(this._repository);
User? _user; User? get user => _user;
bool _isLoading = false; bool get isLoading => _isLoading;
String? _errorMessage; String? get errorMessage => _errorMessage; bool get hasError => _errorMessage != null;
Future<void> loadUser(String userId) async { _isLoading = true; _errorMessage = null; notifyListeners();
try { _user = await _repository.getUser(userId); } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; notifyListeners(); } }
Future<void> updateUserName(String newName) async { if (_user == null) return;
_isLoading = true; notifyListeners();
try { _user = await _repository.updateUser(_user!.id, name: newName); } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; notifyListeners(); } }}
// Viewclass UserProfileView extends StatelessWidget { final String userId;
UserProfileView({required this.userId});
@override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => UserProfileViewModel(context.read<UserRepository>()), child: _UserProfileContent(userId: userId), ); }}
class _UserProfileContent extends StatefulWidget { final String userId;
_UserProfileContent({required this.userId});
@override _UserProfileContentState createState() => _UserProfileContentState();}
class _UserProfileContentState extends State<_UserProfileContent> { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read<UserProfileViewModel>().loadUser(widget.userId); }); }
@override Widget build(BuildContext context) { return Consumer<UserProfileViewModel>( builder: (context, viewModel, child) { if (viewModel.isLoading) { return Center(child: CircularProgressIndicator()); }
if (viewModel.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Erreur: ${viewModel.errorMessage}'), ElevatedButton( onPressed: () => viewModel.loadUser(widget.userId), child: Text('Réessayer'), ), ], ), ); }
final user = viewModel.user; if (user == null) { return Center(child: Text('Aucun utilisateur trouvé')); }
return Column( children: [ Text('Nom: ${user.name}'), Text('Email: ${user.email}'), ElevatedButton( onPressed: () => _showEditDialog(context, viewModel), child: Text('Modifier'), ), ], ); }, ); }
void _showEditDialog(BuildContext context, UserProfileViewModel viewModel) { // Dialog pour modifier le nom }}Sealed Classes pour la gestion d’état asynchrone
Section titled “Sealed Classes pour la gestion d’état asynchrone”Les sealed classes permettent de modéliser différents états de manière type-safe :
sealed class AsyncTask<T> {}
class PendingTask<T> extends AsyncTask<T> {}
class TaskError<T> extends AsyncTask<T> { final String error; TaskError([this.error = 'Error !']);}
class AsyncResult<T> extends AsyncTask<T> { final T data; AsyncResult(this.data);}Utilisation avec ChangeNotifier
Section titled “Utilisation avec ChangeNotifier”class AsyncDataController<T> extends ChangeNotifier { AsyncTask<T> _state = PendingTask<T>(); AsyncTask<T> get state => _state;
Future<void> execute(Future<T> Function() operation) async { _state = PendingTask<T>(); notifyListeners();
try { final result = await operation(); _state = AsyncResult<T>(result); } catch (e) { _state = TaskError<T>(e.toString()); }
notifyListeners(); }}Widget avec pattern matching
Section titled “Widget avec pattern matching”class AsyncDataWidget<T> extends StatelessWidget { final AsyncDataController<T> controller; final Widget Function(T data) dataBuilder; final Widget Function(String error)? errorBuilder; final Widget? loadingWidget;
AsyncDataWidget({ required this.controller, required this.dataBuilder, this.errorBuilder, this.loadingWidget, });
@override Widget build(BuildContext context) { return Consumer<AsyncDataController<T>>( builder: (context, controller, child) { return switch (controller.state) { PendingTask<T>() => loadingWidget ?? CircularProgressIndicator(), TaskError<T>(error: final error) => errorBuilder?.call(error) ?? Text('Erreur: $error'), AsyncResult<T>(data: final data) => dataBuilder(data), }; }, ); }}Alternatives à Provider
Section titled “Alternatives à Provider”InheritedWidget (solution native)
Section titled “InheritedWidget (solution native)”La solution native Flutter pour partager des données dans l’arbre de widgets :
class UserInheritedWidget extends InheritedWidget { final User user;
UserInheritedWidget({ required this.user, required Widget child, }) : super(child: child);
@override bool updateShouldNotify(UserInheritedWidget oldWidget) { return user != oldWidget.user; }
static UserInheritedWidget? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<UserInheritedWidget>(); }}Bloc Pattern
Section titled “Bloc Pattern”Solution basée sur les streams pour une gestion d’état réactive :
// Avec le package flutter_blocclass CounterCubit extends Cubit<int> { CounterCubit() : super(0);
void increment() => emit(state + 1); void decrement() => emit(state - 1);}
// UtilisationBlocProvider( create: (context) => CounterCubit(), child: BlocBuilder<CounterCubit, int>( builder: (context, count) { return Text('$count'); }, ),)Riverpod
Section titled “Riverpod”Version améliorée de Provider avec meilleure sécurité de type :
final counterProvider = StateProvider<int>((ref) => 0);
class CounterWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider);
return Column( children: [ Text('$count'), ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).state++, child: Text('Increment'), ), ], ); }}Gestion d’état réactive avec génération de code :
import 'package:mobx/mobx.dart';
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store { @observable int count = 0;
@action void increment() { count++; }}Bonnes pratiques architecturales
Section titled “Bonnes pratiques architecturales”1. Séparez la logique de l’interface
Section titled “1. Séparez la logique de l’interface”// ✅ Bon : logique séparéeclass TaskListController extends ChangeNotifier { final TaskRepository _repository;
TaskListController(this._repository);
List<Task> _tasks = []; List<Task> get tasks => List.unmodifiable(_tasks);
Future<void> loadTasks() async { _tasks = await _repository.getTasks(); notifyListeners(); }}
// ❌ Mauvais : logique dans le widgetclass TaskListWidget extends StatefulWidget { @override _TaskListWidgetState createState() => _TaskListWidgetState();}
class _TaskListWidgetState extends State<TaskListWidget> { List<Task> tasks = [];
@override void initState() { super.initState(); // ❌ Logique métier dans le widget loadTasks(); }
Future<void> loadTasks() async { final response = await http.get('/tasks'); setState(() { tasks = parseTasksFromJson(response.body); }); }}2. Testez votre logique métier
Section titled “2. Testez votre logique métier”void main() { group('TaskListController', () { late TaskListController controller; late MockTaskRepository mockRepository;
setUp(() { mockRepository = MockTaskRepository(); controller = TaskListController(mockRepository); });
test('should load tasks from repository', () async { final expectedTasks = [Task(id: '1', title: 'Test Task')]; mockRepository.setTasks(expectedTasks);
await controller.loadTasks();
expect(controller.tasks, equals(expectedTasks)); }); });}3. Minimisez les rebuilds
Section titled “3. Minimisez les rebuilds”// ✅ Bon : utilise Selector pour optimiserSelector<CartController, int>( selector: (context, cart) => cart.itemCount, builder: (context, itemCount, child) { return Text('$itemCount items'); },)
// ❌ Mauvais : rebuild inutileConsumer<CartController>( builder: (context, cart, child) { return Text('${cart.itemCount} items'); // Rebuild même si seul itemCount change },)4. Gérez le cycle de vie
Section titled “4. Gérez le cycle de vie”class MyController extends ChangeNotifier { final StreamSubscription _subscription;
MyController() { _subscription = someStream.listen(_handleData); }
@override void dispose() { _subscription.cancel(); super.dispose(); }
void _handleData(dynamic data) { // Traiter les données notifyListeners(); }}5. Organisez vos providers
Section titled “5. Organisez vos providers”// ✅ Bon : hiérarchie logiqueMultiProvider( providers: [ // Services de base (sans dépendances) Provider<ApiService>(create: (context) => ApiService()), Provider<LocalStorage>(create: (context) => LocalStorage()),
// Services avec dépendances ProxyProvider2<ApiService, LocalStorage, UserRepository>( update: (context, api, storage, previous) => UserRepository(api, storage), ),
// Controllers ChangeNotifierProxyProvider<UserRepository, UserController>( create: (context) => UserController(), update: (context, repository, controller) => controller..updateRepository(repository), ), ], child: MyApp(),)Conclusion
Section titled “Conclusion”Le choix de la solution de gestion d’état dépend de la complexité de votre application :
Recommandations par complexité
Section titled “Recommandations par complexité”- Applications simples :
setStateetStatefulWidget - Applications moyennes :
ValueNotifier,ChangeNotifieret Provider - Applications complexes : Provider avec patterns avancés, Bloc, ou Riverpod
Principes à retenir
Section titled “Principes à retenir”- Séparation des responsabilités : Vue, Logique, Données
- Testabilité : Logique métier indépendante des widgets
- Performance : Minimiser les rebuilds inutiles
- Maintenabilité : Architecture claire et patterns cohérents
L’important est de maintenir une architecture claire qui facilite la maintenance, les tests et l’évolution de votre application Flutter.