State management is a hot topic in the Flutter community. You have the default StatefulWidget and then there are literally hundreds of different libraries to choose from. This article will cut through the noise and, once and for all, explain the principles which are valid across many state management solutions. You're also going to see examples of some of the most popular patterns and libraries.

The drawbacks of coupling and mutability

Whether you use StatefulWidget, ChangeNotifier, MobX Store or even a ReactiveModel from states_rebuilder, you couple the fields which hold state (e.g. isLoading boolean) with the class that's responsible for mutating the state. In other words, you usually have only one set of fields per ChangeNotifier / other view model of your choice.

Many people (myself included in the past) use the term provider to describe state management with ChangeNotifier. All of this stems from the unfortunate wording at Google IO 2019. The truth is, that provider just "provides" anything you want down the widget tree and it has nothing to do with state management. You can watch the author of the provider package explain this once and for all in this video.

This single set of fields forces you to do crazy hacks for simple things like an initial or "empty" state - will you create a separate isInitial field...

my_change_notifier.dart

class MyChangeNotifier extends ChangeNotifier {
  bool _isInitial = true;
  bool get isInitial => _isInitial;

  bool _isLoading = false;
  bool get isLoading => isLoading;

  MyEntity _result;
  MyEntity get result => _result;

  String _errorMessage;
  String get errorMessage => _errorMessage;

  void getData() {
    // Bunch of code here
  }
}

...or will you set all the fields to null?

my_change_notifier.dart

class MyChangeNotifier extends ChangeNotifier {
  // Do null checks in the UI to find out if the state is "initial"
  bool _isLoading;
  bool get isLoading => isLoading;

  MyEntity _result;
  MyEntity get result => _result;

  String _errorMessage;
  String get errorMessage => _errorMessage;

  void getData() {
    // Bunch of code here
  }
}

It doesn't matter, either one is a bad choice.

As if that wasn't enough, the ChangeNotifier is also very bloated and you also lose out on the benefits of immutable data like easily achievable undo/redo. Imagine how you'd store the history of state changes if you can update any of the fields independently from one another. Not a very soothing idea to think about indeed.

Bloc or Redux in Flutter

These drawbacks are the reason why people like to use Redux or BLoC for state management despite the fact that these solutions come with drawbacks of their own - boilerplate. More on that later 😉

The basic premise of these state management packages is to separate the inputs, logic and outputs into self-contained classes/functions. This mitigates all the issues outlined when we talked about ChangeNotifier.

Overcoming limitations of mutable data

#1 The first problem we faced with coupled and mutable solutions was that representing an initial or "empty" state is not easily achievable - you have to choose between bad and worse. Since BLoC can separate the states into multiple classes instead of putting everything into one pile of fairly unrelated fields, representing an initial state has never been easier.

I'll focus on BLoC but similar concepts are applicable to Redux as well (except for the fact that Redux has the concept of "single source of truth" for the entire app, whereas there can be multiple BLoCs).
Also, I'll stick to regular classes and inheritance but you can just as easily use freezed unions.
If the acronym BLoC gives you shivers because you've tried to implement it yourself from scratch with Streams, learn about the Bloc package first before going further.

my_state.dart

@immutable
abstract class MyState {}

// value equality override is needed but it's omitted here
// (that's why you should use Freezed)
class Initial extends MyState {}

class Loading extends MyState {}

class Success extends MyState {
  final MyEntity result;

  Success(this.result);
}

class Error extends MyState {
  final String message;

  Error(this.message);
}

The UI will receive only one of these states at a point of time. You don't need to worry about null checking everything or looking for the value of particular field such as isInitial. All you do is check if state is Initial or state is Success and go from there. 


#2 The second limitation, bloatedness, stems from the fact that a single ChangeNotifier or MobX Store class is responsible for everything from holding the state to performing logic.

You've already seen how states are separated into individual classes. The same goes for events, which are the "methods" of a Bloc - they control what's going to happen next. Of course, the component where the business logic happens (BLoC) is a separate class as well. Sure, there may be more code in absolute terms but its complexity is never overwhelming because it's spread out across classes (and files, usually).

my_event.dart

@immutable
abstract class MyEvent {}

class GetData extends MyEvent {}

my_bloc.dart

class MyBloc extends Bloc<MyEvent, MyState> {
  @override
  MyState get initialState => Initial();

  @override
  Stream<MyState> mapEventToState(
    MyEvent event,
  ) async* {
    if (event is GetData) {
      yield Loading();
      // Bunch of code here
    }
  }
}

#3 The last pressing limitation was the inability to implement undo/redo functionality. Well, since the states are emitted only one at a time, there's nothing simpler than storing the states in a stack/list. You don't need to track the individual fields, you just keep record of the MyState subclasses which are emitted from the Stream.

my_bloc.dart

class MyBloc extends Bloc<MyEvent, MyState> {
  ...
  final stateHistory = <MyState>[Initial()];

  @override
  void onTransition(Transition<MyEvent, MyState> transition) {
    stateHistory.add(transition.nextState);
  }
}

State management is about tradeoffs

You can surely see that BLoC introduces quite a lot of boilerplate but stating this fact without any context is not doing this state management library a service. That's like saying that driving a car adds difficulties to your life without stating any of the benefits. Sure, you need to shell out money for the car and you need to take care of it, but at the same time, it allows you to get where you want much more quickly.

If you want to go across the street, hopping into a car would be crazy. If you're going to a destination which is 200 kilometers (124.27 miles 😉) away, you'd be weird if you insisted on going by foot.

Similarly, if you're building an app which is more for display than for functionality, going with the least boilerplatey option makes sense. Building a complex production-grade app requires different solutions altogether.

The benefits of Bloc

The Bloc library provides very good 🦄 tooling and compared to other state management solutions that use Streams, it's a pure gem.

  • Reactive out of the box
    • Bloc is a subclass of Stream, so you know it integrates nicely with just about anything you throw at it without any complications. Examples include:
      • map a Firestore real-time Stream of data into a Stream of states.
      • Apply RxDart operators on Stream<State> and Stream<Event>. For example, debounceTime is useful for auto-search where you want to start searching for a query only after the user has stopped typing for a while.
  • Analytics without any additional work
    • With the help of the BlocDelegate, you can know about any action the user takes in any BLoC without cluttering up your codebase with analytics code.
  • Testing Streams is easy, testing Blocs is even easier
    • Dart is built with Streams in mind and testing if a particular sequence of states has been emitted is simple with stream matchers.
    • Why would you leave it at just the matchers though when you can use the bloc_test package which, in my opinion, makes tests fun to write?
  • BIASED OPINION: The best documentation ever

So, if you want to have the benefits of immutable state, closely work with Streams whether for the purposes of testing or integration with other code, observe state changes happening across the app from a single place ( BlocDelegate ) easily to allow for pain-free analytics and at the same time keep your code as simple as possible, BLoC is truly the best available option out there.

Additionally, you don't have to use just one state management package. While you may use Bloc for managing authentication state, nothing stops you from from keeping a dynamically changing color of a button in a good old StatefulWidget or even a ChangeNotifier. My rule is to manage simple state with simple solutions.

StateNotifier - lean & immutable

But what if you don't need anything that working with Streams has to offer but you still want to keep your state immutable? Is there a crossbreed of Bloc and ChangeNotifier?

Many people who are using Bloc would gladly trade its boilerplate for leaner code while not losing out on immutability. At the same time, people who swear by ChangeNotifier may want to reap the benefits of immutable data but they're hesitant to switch to Bloc.

This is exactly where the StateNotifier steps in. It inherits the best features of Bloc and ChangeNotifier while compromising only the Stream-specific features like the ability to use RxDart operators.

The state_notifier package is authored by Rémi Rousselet, the creator of provider, so you know they will work together nicely. For example, if you're using a ProxyProvider for injecting dependencies into a ChangeNotifier, you'll no longer need to do that if you migrate to StateNotifier.

An in-depth tutorial on how to use state_notifier in Flutter is coming shortly. Subscribe to the newsletter if you don't want to miss it and also to be informed about weekly Flutter news and resources.

StateNotifier has only a single way to communicate data back to the UI - the state field, much like a Bloc has a single Stream of states. This means that the state classes used for Bloc are just as valid for use with a StateNotifier. As a refresher:

my_state.dart

@immutable
abstract class MyState {}

// equality override is needed but it's omitted here
// (that's why you should use Freezed)
class Initial extends MyState {}

class Loading extends MyState {}

class Success extends MyState {
  final MyEntity result;

  Success(this.result);
}

class Error extends MyState {
  final String message;

  Error(this.message);
}

On the contrary to the Bloc package, state_notifier doesn't have the concept of events. Instead, you create regular methods just like with a ChangeNotifier

my_state_notifier.dart

class MyStateNotifier extends StateNotifier<MyState> {
  // initial state gets passed to the super constructor
  MyStateNotifier() : super(Initial());

  void getData() {
    state = Loading();
    // Bunch of code here
  }
}

Best of both worlds?

Since there are no streams, there's substantially less boilerplate than if you had used Bloc. For many people who are on the fence between Bloc and ChangeNotifier, the StateNotifier may be the just the right choice.

If, however, you'd like to use anything Stream-specific which I outlined above (like RxDart operators or great testing experience), then I'd rather stick with Bloc.

Bottom line

I know it's insane. An article about state management which has a bottom line 🎉 Seriously though, the heading is just a joke. I really don't have just one recommendation.

In this post, we explored mutable and immutable state management solutions for Flutter. We focused on ChangeNotifier for the mutable part, but all of the other ones are basically the same in their concept. Then we explored the world of immutable state mostly through the lens of Bloc and its Stream-free alternative, the StateNotifier.

If you like mutating your state and you swear by MobX, that's great. Keep using it. If you like Redux, don't be discouraged by people who are badmouthing it. ChangeNotifier is also great for plenty of simpler apps. The Bloc library is a great choice if you want to build a robust application. State Notifier is awesome if you want immutability without needing to worry about Streams.

Yes, it has ended just like any other state management tutorial but there's one difference. Hopefully, by understanding the main dividing factors of the different state management solutions, you can now make an educated choice for your next Flutter app.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

You may also like

Flutter Custom & Staggered Page Transition Animation Tutorial

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

Dio Connectivity Retry Interceptor – Flutter Tutorial

  • Hello, thanks for great staff. But I have question about my_change_notifier.dart example: Why could not we encapsulate state into MyState based hierarchy of classes, similarly to one at my_state.dart? It would both solve issue of initial state and have all state modification clean using something like MyState updateState (Event myEvent, MyState curState)?

  • BLoC is better than ChangeNotifier approach only in case when you need an undo/redo functionality. That’s no brainer.

    But as for the rest, it’s not.

    1. The issue of “initial or “empty” state”: Why in the world do you use “isInitial” and such fields? Whereas you can have a MyState state; field and a corresponding enum MyState {Initial, Loading, Loaded, Etc,}.

    2. Bloated class issue: you’re not creating all your page in one dart file, right? The same thing with ChangeNotifier – your page may consist of some parts, make a reasonable number of parts and ChangeNotifiers (ViewModels) for them and you’re good.

    and as a bonus: if you use Provider + ChangeNotifier you may want to use ProxyProvider to inject your services inside ChangeNotifiers but let’s be honest – get_it service locator makes it better and more cleaner.

    Happy coding and thank you for the article!

    • You’re totally right about the enum for the state, I should’ve put it in the tutorial. Still, nothing beats “physically” limiting the fields I can access by using separate state classes with Bloc/StateNotifier.

      As for the second point, I create multiple BLoCs for one page too.

      Couldn’t agree more about get_it.

      In the end, it’s about preferences. I truly enjoy the aspects of immutability and close integration with Streams which BLoC provides. At the same time, I see why many people would rather use ChangeNotifier or MobX.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >