Gad Ntenta
Développeur Full Stack
State Management dans Flutter
FlutterState ManagementBlocProvider
Guide complet sur la gestion d'état dans Flutter : Provider, Bloc, Riverpod, et plus encore.
Gestion d'État dans Flutter
Introduction
La gestion d'état est un aspect crucial du développement d'applications Flutter. Dans cet article, nous allons explorer les différentes approches et solutions pour gérer efficacement l'état de vos applications.
Approches de Gestion d'État
1. setState
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Compteur: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Incrémenter'),
),
],
);
}
}
2. Provider
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
void decrement() {
_counter--;
notifyListeners();
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CounterProvider(),
child: Consumer<CounterProvider>(
builder: (context, provider, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Compteur: ${provider.counter}'),
ElevatedButton(
onPressed: provider.increment,
child: Text('Incrémenter'),
),
ElevatedButton(
onPressed: provider.decrement,
child: Text('Décrémenter'),
),
],
);
},
),
);
}
}
3. Bloc
// Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// States
abstract class CounterState {
final int count;
CounterState(this.count);
}
class CounterInitial extends CounterState {
CounterInitial() : super(0);
}
class CounterUpdated extends CounterState {
CounterUpdated(int count) : super(count);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<IncrementEvent>((event, emit) {
emit(CounterUpdated(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterUpdated(state.count - 1));
});
}
}
// UI
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Compteur: ${state.count}'),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementEvent());
},
child: Text('Incrémenter'),
),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(DecrementEvent());
},
child: Text('Décrémenter'),
),
],
);
},
),
);
}
}
4. Riverpod
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Compteur: $count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: Text('Incrémenter'),
),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).decrement();
},
child: Text('Décrémenter'),
),
],
);
}
}
Gestion d'État Avancée
1. Gestion d'État avec GetX
class CounterController extends GetxController {
final count = 0.obs;
void increment() => count.value++;
void decrement() => count.value--;
}
class CounterScreen extends GetView<CounterController> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() => Text('Compteur: ${controller.count}')),
ElevatedButton(
onPressed: controller.increment,
child: Text('Incrémenter'),
),
ElevatedButton(
onPressed: controller.decrement,
child: Text('Décrémenter'),
),
],
);
}
}
2. Gestion d'État avec MobX
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int count = 0;
@action
void increment() {
count++;
}
@action
void decrement() {
count--;
}
}
class CounterScreen extends StatelessWidget {
final store = CounterStore();
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Compteur: ${store.count}'),
ElevatedButton(
onPressed: store.increment,
child: Text('Incrémenter'),
),
ElevatedButton(
onPressed: store.decrement,
child: Text('Décrémenter'),
),
],
),
);
}
}
Meilleures Pratiques
1. Choix de la Solution
- Évaluer les besoins du projet
- Considérer la complexité de l'application
- Prendre en compte l'expérience de l'équipe
- Vérifier la maintenance et le support
2. Organisation du Code
- Séparer la logique métier de l'UI
- Utiliser des modèles de données
- Implémenter des repositories
- Suivre les principes SOLID
3. Performance
- Éviter les rebuilds inutiles
- Utiliser des widgets const
- Optimiser les listeners
- Mettre en cache les données
4. Tests
- Écrire des tests unitaires
- Tester les états
- Vérifier les transitions
- Simuler les scénarios
Exemple Complet
1. Application Todo avec Bloc
// Models
class Todo {
final String id;
final String title;
final bool completed;
Todo({
required this.id,
required this.title,
this.completed = false,
});
Todo copyWith({
String? id,
String? title,
bool? completed,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
}
}
// Events
abstract class TodoEvent {}
class LoadTodos extends TodoEvent {}
class AddTodo extends TodoEvent {
final String title;
AddTodo(this.title);
}
class ToggleTodo extends TodoEvent {
final String id;
ToggleTodo(this.id);
}
class DeleteTodo extends TodoEvent {
final String id;
DeleteTodo(this.id);
}
// States
abstract class TodoState {
final List<Todo> todos;
TodoState(this.todos);
}
class TodoInitial extends TodoState {
TodoInitial() : super([]);
}
class TodoLoading extends TodoState {
TodoLoading() : super([]);
}
class TodoLoaded extends TodoState {
TodoLoaded(List<Todo> todos) : super(todos);
}
class TodoError extends TodoState {
final String message;
TodoError(this.message) : super([]);
}
// Bloc
class TodoBloc extends Bloc<TodoEvent, TodoState> {
final TodoRepository repository;
TodoBloc(this.repository) : super(TodoInitial()) {
on<LoadTodos>(_onLoadTodos);
on<AddTodo>(_onAddTodo);
on<ToggleTodo>(_onToggleTodo);
on<DeleteTodo>(_onDeleteTodo);
}
Future<void> _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async {
emit(TodoLoading());
try {
final todos = await repository.getTodos();
emit(TodoLoaded(todos));
} catch (e) {
emit(TodoError(e.toString()));
}
}
Future<void> _onAddTodo(AddTodo event, Emitter<TodoState> emit) async {
try {
final todo = Todo(
id: DateTime.now().toString(),
title: event.title,
);
await repository.addTodo(todo);
final todos = await repository.getTodos();
emit(TodoLoaded(todos));
} catch (e) {
emit(TodoError(e.toString()));
}
}
Future<void> _onToggleTodo(ToggleTodo event, Emitter<TodoState> emit) async {
try {
await repository.toggleTodo(event.id);
final todos = await repository.getTodos();
emit(TodoLoaded(todos));
} catch (e) {
emit(TodoError(e.toString()));
}
}
Future<void> _onDeleteTodo(DeleteTodo event, Emitter<TodoState> emit) async {
try {
await repository.deleteTodo(event.id);
final todos = await repository.getTodos();
emit(TodoLoaded(todos));
} catch (e) {
emit(TodoError(e.toString()));
}
}
}
// UI
class TodoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TodoBloc(TodoRepository())..add(LoadTodos()),
child: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
if (state is TodoLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is TodoError) {
return Center(child: Text(state.message));
}
if (state is TodoLoaded) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed
? TextDecoration.lineThrough
: null,
),
),
leading: Checkbox(
value: todo.completed,
onChanged: (_) {
context.read<TodoBloc>().add(ToggleTodo(todo.id));
},
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
context.read<TodoBloc>().add(DeleteTodo(todo.id));
},
),
);
},
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
labelText: 'Nouvelle tâche',
suffixIcon: IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Ajouter la tâche
},
),
),
onSubmitted: (value) {
if (value.isNotEmpty) {
context.read<TodoBloc>().add(AddTodo(value));
}
},
),
),
],
);
}
return Container();
},
),
);
}
}
Conclusion
La gestion d'état est un aspect fondamental du développement Flutter. En choisissant la bonne approche et en suivant les meilleures pratiques, vous pouvez créer des applications maintenables et performantes.
N'oubliez pas de :
- Choisir la solution adaptée à votre projet
- Organiser votre code de manière claire
- Optimiser les performances
- Écrire des tests
Avec ces connaissances, vous êtes prêt à gérer efficacement l'état de vos applications Flutter !