12 ways to manage state in Flutter
Published on August 16, 22'
flutter

Table of contents

Introduction

One of the most important things to consider when making a Flutter application (or an application of the same sort) is how to manage state, that is to say data that changes in accordance to specified events or all purpose data that is shared across the entire app.

Some of these methods will be described and they are as follows:

  • StatefulWidget
  • StatefulBuilder
  • InheritedWidget
  • RxDart BehaviourSubject
  • BLoC
  • Redux
  • Mobx
  • Scoped Model
  • Flutter Hooks
  • Provider
  • RiverPod
  • Firebase

To demonstrate, a series of examples will be shown with a rudimentary use case - an app that tracks an incrementing counter.

StatefulWidget

StatefulWidget is the most simple and common way of organizing state, however it is problematic because it needs a significant amount of boilerplate: two classes and a frequent call to the setState function. That and it does not scale well when the application grows.

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _value = 1;

  void increment() {
    setState(() {
      _value++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text("$_value");
  }
}

StatefulBuilder

A simpler version of the StatefulWidget, so to say. Logic is not separated into a new class and all of the state is located inside the StatefulBuilder object.

Not ideal for scaling, but it is useful for building smaller parts of local state.

class Counter extends StatelessWidget {
  int _value = 1;

  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StatefulBuilder(
      builder: (context, StateSetter setState) => Column(
        children: [
          Text("$_value"),
          TextButton(
            onPressed: () => setState(() => _value++),
            child: Text("Press me"),
          ),
        ],
      ),
    );
  }
}

InheritedWidget

Solves the problem of component communication if they are deeply nested within the component tree. There are alternatives and it is not ideal for common use, however it is implemented within a lot of built-in Flutter components.

It allows the sharing of state with child components, regardless of their depth.

class InheritedCounter extends InheritedWidget {
  final Map _counter = {"val": 0};
  final Widget child;

  increment() {
    _counter["val"]++;
  }

  get counter => _counter["val"];

  InheritedCounter({
    required this.child,
    Key? key,
  }) : super(
    child: child,
    key: key,
  );

  @override
  bool updateShouldNotify(InheritedCounter oldWidget) => true;

  static InheritedCounter? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedCounter>();
  }
}

class Counter extends StatelessWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {
        int counter = InheritedCounter.of(context)?.counter;
        Function? increment = InheritedCounter.of(context)?.increment;
        return Column(children: [
          Text("$counter"),
          TextButton(
            onPressed: () => setState(() => increment!()),
            child: Text("Press me"),
          ),
        ]);
      },
    );
  }
}

An object is declared with its own state extending the InheritedWidget class, then it is exposed to the global scope of the application with the static of method.

In the same object a function that changes the state is provided, along with the reactive state. Both can be accessed in a component the user chooses.

RxDart BehaviourSubject

This approach uses an easily understandable flow of data and it allows for easier testing and isolation of content.

In addition, it tightly integrates into the existing Flutter streams and builders and can be used as a replacement for the BLoC pattern.

class CounterService {
  final BehaviourSubject _counter = BehaviourSubject.seeded(1);

  Observable get stream$ => _counter.stream;
  int get current => _counter.value;

  increment() {
    _counter.add(current + 1);
  }
}

CounterService counterService = CounterService();

class Countner extends StatelessWidget {
  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: counterService.stream$,
      builder: (BuildContext context, AsyncSnapshot snap) {
        return Column(
          children: [
            Text(snap.data),
            TextButton(
              onPressed: () => counterService.increment(),
              child: Text("Press me"),
            ),
          ],
        );
      },
    );
  }
}

The important features are encapsulated inside a class that possesses the BehaviourSubject object. Using this object the user can monitor how the data changes and change the data itself. To access the data externally (inside a component) getters should be provided.

Acquiring the data afterwards is a breeze as the only thing the user must do is create an instance of the service then use the getters or the methods that mutate data.

The package is provided here.

BLoC

Short for Business Logic Componnent, it allows for the use of a one way data flow with the dedicated Cubit class. It is very reusable and testable, changes in the state can be monitored very easily but it requires a bit more boilerplate.

bloc-1

Everything revolves around the wrapper component called BlocProvider and instances of the Cubit class as it is the fundamental building block of state.

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

class Counter extends StatelessWidget {
  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: BlocBuilder<CounterCubit, int>(
        builder: (BuildContext context, int count) {
          return Column(
            children: [
              Text("$count"),
              TextButton(
                onPressed: () => {
                  context.read<CounterCubit>().increment();
                },
                child: Text("Press me"),
              ),
            ],
          );
        },
      ),
    );
  }
}

The package is provided here.

Redux

The de facto package for managing state all across popular Javascript frameworks makes its way into Flutter. To use the package some component wrapping is required, but in essence it is similar to BloC.

enum Actions { increment }

int counterReducer(int state, dynamic action) {
  if (action == Actions.increment) {
    return state + 1;
  }
  return state;
}

class Counter extends StatelessWidget {
  final Store<int> store = Store<int>(counterReducer, initialState: 0);

  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<int>(
      store: store,
      child: StoreConnector<int, String>(
        converter: (store) => store.state.toString(),
        builder: (context, count) {
          return Column(
            children: [
              Text(count),
              StoreConnector<int, VoidCallback>(
                converter: (store) {
                  return () => store.dispatch(Actions.increment);
                },
                builder: (context, callback) {
                  return TextButton(
                    onPressed: callback,
                    child: Text("Press me!"),
                  );
                },
              ),
            ],
          );
        },
      ),
    );
  }
}

The package is provided here.

Mobx

Seemingly more developer friendly, Mobx adds an approach that relies on decorated properties inside an object that tracks the current state.

mobx

There are three core concepts with Mobx:

  • Observables - reactive data
  • Actions - functions that change the data
  • Reactions - functions that track the current state in real time
class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  final _counter = Counter();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Observer(
          builder: (_) => Text("$_counter.value"),
        ),
        TextButton(
          onPressed: _counter.increment,
          child: Text("Press me"),
        ),
      ],
    );
  }
}

The package is provided here.

Scoped Model

This method is recommended in the documentation, it requires almost no additional boilerplate and it implements a number of Flutters features under the hood - such as the Listenable interface for the sake of data reactivity and the AnimatedBuilder class.

class CounterModel extends Model {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
}

class Counter extends StatelessWidget {
  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ScopedModel<CounterModel>(
      model: CounterModel(),
      child: ScopedModelDescendant<CounterModel>(
        builder: (context, child, model) => Column(
          children: [
            Text("$model.counter"),
            TextButton(
              onPressed: model.incremennt,
              child: Text("Press me"),
            ),
          ],
        ),
      ),
    );
  }
}

The package is provided here.

Flutter Hooks

This one should be a familiar sight to React or React Native developers because the package was inspired by their hook system.

Several hooks are provided, with the most prominent one being useState. Very simple and easy to use.

class Counter extends HookWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final _counter = useState(0);

    void increment() {
      _counter.value++;
    }

    return Column(
      children: [
        Text("$_counter.value"),
        TextButton(
          onPressed: increment,
          child: Text("Press me"),
        ),
      ],
    );
  }
}

The package is provided here.

Provider

As stated by its creator who also created Flutter Hooks, this package is a wrapper around InheritedWidget that makes the whole process of using it easier and more reusable.

Among other things provider offers vastly simplified allocation and disposal of resources, lazy loading, boilerplate and predefined code snippets to use.

The official Flutter site even showcases this package.

class CounterViewModel extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }
}

class Counter extends StatelessWidget {
  Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CounterViewModel>(
      create: (BuildContext context) => CounterViewModel(),
      child: Column(
        children: [
          Consumer<CounterViewModel>(builder: (context, viewModel, child) {
            return Text('${viewModel.counter}');
          }),
          TextButton(
            onPressed: Provider.of<CounterViewModel>(context, listen: false).incrementCounter,
            child: Text("Press me"),
          ),
        ],
      ),
    );
  }
}

The package is provided here.

RiverPod

Again by the same creator, RiverPod is an upgrade of sorts to the Provider package.

The package is even able to be used alongside the Flutter hooks, mentioned before.

class Notifier extends StateNotifier<int> {
  Notifier() : super(0);

  void increment() {
    state = state + 1;
  }
}

final provider = StateNotifierProvider<Notifier, int>((ref) {
  return Notifier();
});

class Counter extends ConsumerWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var counter = ref.watch(provider);

    return Column(
      children: [
        Text("$counter"),
        TextButton(
          onPressed: () => ref.read(provider.notifier).increment(),
          child: Text(
            "Press me",
            style: TextStyle(color: Colors.white),
          ),
        ),
      ],
    );
  }
}

Unlike Provider, no wrapping is needed in the build method but the widget itself has to extend the ConsumerWidget class.

The package is provided here.

Firebase

Even though it seems like a strange choice, Firebase can also be used for state management as it allows for the tracking of real time data and it provides an authentification and authorization system right out of the box.

One package that comes to mind when mentioning this is FlutterFire.

Conclusion

There are a lot of ways that one can organize their state, by looking at a plethora of those in this post hopefully some will be useful in future endeavours. Most approaches described are very similar but each one shines in its own regard and is used in specific cases, depending on what the user wants to build.

©   Matija Novosel 2024