Flutter State Management 2021

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.

  1. Riverpod
  2. Cubit/BLoC
  3. GetX

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 of StatelessWidget or a Consumer 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 the onError 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 it
    var 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

  1. 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()));
  1. 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++;
    }
    
  2. Create your widget and just use StatelessWidget instead of StatefulWidget (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}")));
        }
    }
    
comments powered by Disqus