State Management in Flutter
We have evaluated different state management plugins for Flutter. Two of the most commonly used plugins are Riverpod and BLoC/Cubit.
Riverpod
Riverpod is the successor of flutter providers and builds up on that base while also reducing some issues.
Concept:
- build a global provider (e.g. static variable, object or changing stream
final keyProvider = Provider((ref) => 'myKey');
- wrap up your App() with a
ProviderScope()
void main() { runApp(ProviderScope(child: MyApp())); }
- use
ConsumerWidget
instead ofStatelessWidget
or aConsumer
inside the build method of another widget to read the values from your provider watch
your provider for values or state// *** // ConsumerWidget // *** class Example extends ConsumerWidget { @override Widget build(BuildContext context, ScopedReader watch) { // Listens to the value exposed by counterProvider int count = watch(counterProvider).state; return Scaffold( appBar: AppBar(title: const Text('Counter example')), body: Center( child: Text('$count'), ), ); } } // *** // Consumer // *** class Example extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Counter example')), body: Center( child: Consumer( // Rebuild only the Text when counterProvider updates builder: (context, watch, child) { // Listens to the value exposed by counterProvider int count = watch(counterProvider).state; return Text('$count'); }, ), ), ); } }
Changeable State Example
// create a StateNotifier
class CounterProvider extends StateNotifier<int> {
Counter(): super(0);
void increment() => state++;
}
// create a Provider for the StateNotifier
final counterProvider = StateNotifierProvider((ref) => Counter());
// ...
@override
Widget build(BuildContext context, ScopedReader watch) {
// Obtains Counter without listening to Counter.state.
// Will not cause the button to rebuild when the counter changes.
final Counter counter = watch(counterProvider);
return RaisedButton(
onPressed: counter.increment,
child: Text('increment'),
);
}
Sidenotes
- You can also subscribe to Providers to start a custom action depending on the current state, e.g. open a dialog if user is empty
- You can combine providers and watch provider A from provider B
- There is an option to use riverpod together with flutter_hooks if desired
- even without hooks the concept reminds of React’s context-based state management
- Providers may be modified, e.g. to auto-dispose when not needed anymore
BLoC/Cubit
The bloc plugin helps to differ UI widgets from business logic. Cubits are the base class, while BLoCs are the extended version. Cubits were added later to the same package to provide a simpler alternative sufficient for most cases.
A cubit is a type of stream that requires an initial state before emitting its first state result. Afterwards functions called on the cubit cause the state getting re-calculated and emitted again.
Example
- Create a cubit
/// A `CounterCubit` which manages an `int` as its state. class CounterCubit extends Cubit<int> { /// The initial state of the `CounterCubit` is 0. CounterCubit() : super(0); /// When increment is called, the current state /// of the cubit is accessed via `state` and /// a new `state` is emitted via `emit`. void increment() => emit(state + 1); }
- Using a cubit
void main() { /// Create a `CounterCubit` instance. final cubit = CounterCubit(); /// Access the state of the `cubit` via `state`. print(cubit.state); // 0 /// Interact with the `cubit` to trigger `state` changes. cubit.increment(); /// Access the new `state`. print(cubit.state); // 1 /// Close the `cubit` when it is no longer needed. cubit.close(); }
- Cubits may override the
onChange
and theonError
method to react to any changes to the state@override void onChange(Change<int> change) { super.onChange(change); print(change); }
BLoCs
Instead of functions getting called BLoCs are listening to incoming events. It also adds more lifecycle methods for
possible overwrites like onEvent
, onTransition
and so on. The mapEventToState
function is responsible to alter the
state corresponding to incoming events.
- Creating a bloc
/// The events which `CounterBloc` will react to. enum CounterEvent { increment } /// A `CounterBloc` which handles converting `CounterEvent`s into `int`s. class CounterBloc extends Bloc<CounterEvent, int> { /// The initial state of the `CounterBloc` is 0. CounterBloc() : super(0); @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { /// When a `CounterEvent.increment` event is added, /// the current `state` of the bloc is accessed via the `state` property /// and a new state is emitted via `yield`. case CounterEvent.increment: yield state + 1; break; } } }
- Using a bloc
void main() async { /// Create a `CounterBloc` instance. final bloc = CounterBloc(); /// Access the state of the `bloc` via `state`. print(bloc.state); // 0 /// Interact with the `bloc` to trigger `state` changes. bloc.add(CounterEvent.increment); /// Wait for next iteration of the event-loop /// to ensure event has been processed. await Future.delayed(Duration.zero); /// Access the new `state`. print(bloc.state); // 1 /// Close the `bloc` when it is no longer needed. bloc.close(); }
GetX
Get also provides state management in its package in a simple convenient way. It aims to make development for Flutter easier and more convenient.
Concept
- You start with a variable you want to make observable
var name = 'My Name';
- To do that you just need to add
.obs
to itvar name = 'My Name'.obs;
- In your UI widget you simply refer to it this way to update it every time the value changes
Obx(() => Text("${controller.name}"));
Example
- Add
get
on App level to your MaterialApp. It is a quick way to use a modified widget with the MaterialApp as default child.
void main() => runApp(GetMaterialApp(home: Home()));
- Create a class for your business logic, methods and variables. To make a variable observable simply add
.obs
to it value.class Controller extends GetxController{ var count = 0.obs; increment() => count++; }
- Create your widget and just use
StatelessWidget
instead ofStatefulWidget
(saves some RAM)class Home extends StatelessWidget { @override Widget build(context) { // Instantiate your class using Get.put() to make it available for all "child" routes there. final Controller c = Get.put(Controller()); return Scaffold( // Use Obx(()=> to update Text() whenever count is changed. appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))), // Replace the 8 lines Navigator.push by a simple Get.to(). You don't need context body: Center(child: RaisedButton( child: Text("Go to Other"), onPressed: () => Get.to(Other()))), floatingActionButton: FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment)); } } class Other extends StatelessWidget { // You can ask Get to find a Controller that is being used by another page and redirect you to it. final Controller c = Get.find(); @override Widget build(context){ // Access the updated count variable return Scaffold(body: Center(child: Text("${c.count}"))); } }