State management is crucial for building dynamic, efficient, and bug-free apps. Let's delve into three popular state management techniques: Provider, Riverpod, and Bloc, using the provided examples.
1. Provider π
Provider is a simple yet powerful state management technique. Let's dissect the provided example:
// This code is distributed under the MIT License.// Copyright (c) 2019 Remi Rousselet.// You can find the original at https://github.com/rrousselGit/provider.import'package:flutter/foundation.dart';import'package:flutter/material.dart';import'package:provider/provider.dart';// This is a reimplementation of the default Flutter application// using provider + [ChangeNotifier].voidmain() {runApp(// Providers are above [MyApp] instead of inside it, so that// tests can use [MyApp] while mocking the providersMultiProvider( providers: [ChangeNotifierProvider(create: (context) =>Counter()), ], child:constMyApp(), ), );}// Mix-in [DiagnosticableTreeMixin] to have access to// [debugFillProperties] for the devtool ignore: prefer_mixinclassCounterwithChangeNotifier, DiagnosticableTreeMixin {int _count =0;intget count => _count;voidincrement() { _count++;notifyListeners(); }// Makes `Counter` readable inside the devtools by listing all// of its properties.@overridevoiddebugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(IntProperty('count', count)); }}classMyAppextendsStatelessWidget {constMyApp({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnMaterialApp( theme:ThemeData( colorSchemeSeed:Colors.blue, useMaterial3:true, ), home:constMyHomePage(), ); }}classMyHomePageextendsStatelessWidget {constMyHomePage({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnScaffold( appBar:AppBar( title:constText('Provider example'), ), body:constCenter( child:Column( mainAxisSize:MainAxisSize.min, mainAxisAlignment:MainAxisAlignment.center, children:<Widget>[Text('You have pushed the button this many times:'),// Extracted as a separate widget for performance// optimization. As a separate widget, it will rebuild// independently from [MyHomePage].//// This is totally optional (and rarely needed).// Similarly, we could also use [Consumer] or// [Selector].Count(), ], ), ), floatingActionButton:FloatingActionButton( key:constKey('increment_floatingActionButton'),// Calls `context.read` instead of `context.watch` so// that it does not rebuild when [Counter] changes. onPressed: () => context.read<Counter>().increment(), tooltip:'Increment', child:constIcon(Icons.add), ), ); }}classCountextendsStatelessWidget {constCount({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnText(// Calls `context.watch` to make [Count] rebuild when// [Counter] changes.'${context.watch<Counter>().count}', key:constKey('counterState'), style:Theme.of(context).textTheme.headlineMedium, ); }}
Explanation:
Model (Counter class): This class holds the state (_count) and has a method to update the state (increment). When the state changes, notifyListeners is called to notify the UI to rebuild.
Provider Setup (MultiProvider): MultiProvider is used to provide Counter to the widget tree.
Accessing and Updating State: context.read<Counter>().increment() is used to call the increment method and update the state. context.watch<Counter>().count is used to access the current state and rebuild the UI when it changes.
2. Riverpod π
Riverpod is a flexible and powerful state management solution. Let's analyze the provided example:
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file// for details. All rights reserved. Use of this source code is governed by a// BSD-style license that can be found in the LICENSE file.import'package:flutter/material.dart';import'package:flutter_riverpod/flutter_riverpod.dart';// This is a reimplementation of the default Flutter application// using riverpod.voidmain() {runApp(// Adding ProviderScope enables Riverpod for the entire projectconstProviderScope(child:MyApp()), );}/// Providers are declared globally and specify how to create a statefinal counterProvider =StateProvider((ref) =>0);classMyAppextendsStatelessWidget {constMyApp({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnMaterialApp( theme:ThemeData( colorSchemeSeed:Colors.blue, useMaterial3:true, ), home:constMyHomePage(), ); }}classMyHomePageextendsConsumerWidget {constMyHomePage({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context, WidgetRef ref) {returnScaffold( appBar:AppBar( title:constText('Riverpod example'), ), body:Center( child:Column( mainAxisSize:MainAxisSize.min, mainAxisAlignment:MainAxisAlignment.center, children:<Widget>[constText('You have pushed the button this many times:'),Consumer( builder: (context, ref, _) {final count = ref.watch(counterProvider);returnText('$count', key:constKey('counterState'), style:Theme.of(context).textTheme.headlineMedium, ); }, ), ], ), ), floatingActionButton:FloatingActionButton( key:constKey('increment_floatingActionButton'),// The read method is a utility to read a provider without listening to it onPressed: () => ref.read(counterProvider.notifier).state++, tooltip:'Increment', child:constIcon(Icons.add), ), ); }}
Explanation:
Provider Declaration: counterProvider is declared globally, specifying how to create the state.
Provider Scope: ProviderScope is used to enable Riverpod for the project.
Accessing and Updating State: ref.read(counterProvider.notifier).state++ is used to update the state. A Consumer widget is used to rebuild the UI when the state changes.
Bloc separates business logic from UI, making the codebase more organized and testable. Let's dissect the provided example:
// This code is distributed under the MIT License.// Copyright (c) 2018 Felix Angelov.// You can find the original at https://github.com/felangel/bloc.import'package:flutter/material.dart';import'package:flutter_bloc/flutter_bloc.dart';voidmain() {Bloc.observer =AppBlocObserver();runApp(constApp());}/// Custom [BlocObserver] that observes all bloc and cubit state changes.classAppBlocObserverextendsBlocObserver {@overridevoidonChange(BlocBase bloc, Change change) { super.onChange(bloc, change);if (bloc isCubit) print(change); }@overridevoidonTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition);print(transition); }}/// {@template app}/// A [StatelessWidget] that:/// * uses [bloc](https://pub.dev/packages/bloc) and/// [flutter_bloc](https://pub.dev/packages/flutter_bloc)/// to manage the state of a counter and the app theme./// {@endtemplate}classAppextendsStatelessWidget {/// {@macro app}constApp({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnBlocProvider( create: (_) =>ThemeCubit(), child:constAppView(), ); }}/// {@template app_view}/// A [StatelessWidget] that:/// * reacts to state changes in the [ThemeCubit]/// and updates the theme of the [MaterialApp]./// * renders the [CounterPage]./// {@endtemplate}classAppViewextendsStatelessWidget {/// {@macro app_view}constAppView({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnBlocBuilder<ThemeCubit, ThemeData>( builder: (_, theme) {returnMaterialApp( theme: theme, home:constCounterPage(), ); }, ); }}/// {@template counter_page}/// A [StatelessWidget] that:/// * provides a [CounterBloc] to the [CounterView]./// {@endtemplate}classCounterPageextendsStatelessWidget {/// {@macro counter_page}constCounterPage({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnBlocProvider( create: (_) =>CounterBloc(), child:constCounterView(), ); }}/// {@template counter_view}/// A [StatelessWidget] that:/// * demonstrates how to consume and interact with a [CounterBloc]./// {@endtemplate}classCounterViewextendsStatelessWidget {/// {@macro counter_view}constCounterView({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {returnScaffold( appBar:AppBar(title:constText('Counter')), body:Center( child:BlocBuilder<CounterBloc, int>( builder: (context, count) {returnText('$count', style:Theme.of(context).textTheme.displayLarge); }, ), ), floatingActionButton:Column( crossAxisAlignment:CrossAxisAlignment.end, mainAxisAlignment:MainAxisAlignment.end, children:<Widget>[FloatingActionButton( child:constIcon(Icons.add), onPressed: () => context.read<CounterBloc>().add(Increment()), ),constSizedBox(height:4),FloatingActionButton( child:constIcon(Icons.remove), onPressed: () => context.read<CounterBloc>().add(Decrement()), ),constSizedBox(height:4),FloatingActionButton( child:constIcon(Icons.brightness_6), onPressed: () => context.read<ThemeCubit>().toggleTheme(), ), ], ), ); }}/// Event being processed by [CounterBloc].abstractclassCounterEvent {}/// Notifies bloc to increment state.classIncrementextendsCounterEvent {}/// Notifies bloc to decrement state.classDecrementextendsCounterEvent {}/// {@template counter_bloc}/// A simple [Bloc] that manages an `int` as its state./// {@endtemplate}classCounterBlocextendsBloc<CounterEvent, int> {/// {@macro counter_bloc}CounterBloc() : super(0) {on<Increment>((event, emit) =>emit(state +1));on<Decrement>((event, emit) =>emit(state -1)); }}/// {@template brightness_cubit}/// A simple [Cubit] that manages the [ThemeData] as its state./// {@endtemplate}classThemeCubitextendsCubit<ThemeData> {/// {@macro brightness_cubit}ThemeCubit() : super(_lightTheme);staticfinal _lightTheme =ThemeData.light(useMaterial3:true);staticfinal _darkTheme =ThemeData.dark(useMaterial3:true);/// Toggles the current brightness between light and dark.voidtoggleTheme() {emit(state.brightness ==Brightness.dark ? _lightTheme : _darkTheme); }}
Explanation:
Bloc Declaration: CounterBloc is defined to manage the state of a counter. It defines how to handle Increment and Decrement events to update the state.
Provider Setup: BlocProvider is used to provide CounterBloc to the widget tree.
Accessing and Updating State: context.read<CounterBloc>().add(Increment()) is used to dispatch an Increment event to the CounterBloc, which in turn updates the state.
These breakdowns will help you understand how each part of the code contributes to state management in the respective examples. Each technique has its own set of advantages and suits different use cases.
By understanding and practicing these techniques, you'll be well on your way to mastering state management in Flutter.