Simple & Performant State Management without an External Library

Using Flutter’s InheritedNotifier

Lee Phillips
Geek Culture
9 min readMar 22, 2022

--

Introduction

State management in Flutter is fun! Okay, if you have any experience with implementing state management in Flutter, or if you are currently trying to learn the subject, you probably do not agree with that statement. I don’t blame you! Although I do enjoy a challenge, this one can seem quite daunting and tedious at times.

Fortunately, there are several great packages that make managing state in a large app much easier. (Shout out to provider/riverpod and bloc!) They also provide great features to support testability. If you are not already familiar, provider is is based on InheritedWidgets, which is Flutter’s built-in approach for handling state, by allowing us to find objects higher up the widget tree rather than pass them down the tree widget by widget.riverpod is an improvement to provider that required rewriting and is therefore a new package, and bloc is a state management approach that helps implement the BLoC design pattern, which stands for Business Logic Component. It is built around Streams.

Again, all of these packages are great, and they can certainly be very useful for a large project, especially the testing capabilities they provide. bloc in particular, in my experience, integrates with a number of other packages that can make the development process easier, for instance, hydrated_bloc, which handles persisting state changes to disk. However, these vastly powerful packages are not actually necessary for a solid state management solution. In particular, they are overkill for smaller projects, and it just so happens that Flutter already has all you need to create a powerful state management solution very easily.

We will of course be using InheritedWidgets to manage our app’s state. Wait! Don’t close that tab! If you sought out this article, I’m sure you already have some pretty strong opinions about this approach. “Too complicated.” “Poor maintainability.” “Too much boilerplate!” I hear ya, but what if I told you that with a subclass of InheritedWidget,we’re going to write a performant, adaptable & reusable state management solution in 40 lines or less? Back in? Good! Hopefully, you’ll learn a few cool things along the way as well. Let’s get started!

InheritedWidget

First, let’s talk about a bit about InheritedWidgets. These are simply specialized Widgets that are designed to hold data and make it is easily accessible by any Widget below the InheritedWidget in the widget tree. This prevents us from having to pass the data through say 20 Widget constructors just so the 21st Widget in the tree can access it. Instead, with InheritedWidgets, we can lookup the data directly from the 21st Widget by traversing up the widget tree. However,InheritedWidgets are immutable, meaning that variables must be final, so it can be tricky to wrap your head around how you are supposed to update the data from widgets further down the tree. This means that we must actually store the data in a Widget above the InheritedWidget and provide access to it in a way that allows mutability from below in the widget tree if it needs to change.

I’m not going to go into the complicated process of implementing an InheritedWidget where we can update the data from its dependencies. This article is about a better (and easier!) approach. I should point out however that InheritedWidget is perfect for providing data to descendants when that data will not change from below. For example, providing data to a complexDialog, or defining a theme for descendants to use. We’ve all worked with Theme in Flutter, which is in fact an InheritedWidget that does the latter. If you are interested, here is a simple InheritedWidget example that approximates the behavior of Theme. It has no bearing on the rest of this article.

If we wish to provide the ability for descendants to update the theme, we have 2 equally ugly options:

  1. We pass a callback to the InheritedWidget that allows it to call setState, as well as writing a bunch of extra boilerplate to handle setting/getting of data. An even worse side effect, we must return the InheritedWidget from our static of function rather than the actual data.
  2. We pass the State as the data to InheritedWidget, returning it from the of function and manipulating it directly throughout the app. Ugh!

Both of these approaches mean uglier and more verbose code to access the data. Fortunately, there is a much better solution!

InheritedNotifier

InheritedNotifier is a subclass of InheritedWidget designed specifically for storing a Listenable object and updating its dependents whenever the data changes. It does so efficiently too. If multiple changes occur between 2 frames, the dependents still only rebuild once. As for our Listenable, we can use a ValueNotifier for single values, aChangeNotifier for a more complex data class, or even an Animation to synchronize shared animations among multiple objects. An example of the latter can be found in the Flutter docs here. We will extend ChangeNotifier to create our app state data class.

Performance

If you have heard about ChangeNotifier during your state management research thus far, you have likely come across discussions of performance compared to other approaches. The way Listenables work is by storing a list of Listeners and notifying them one-by-one any time a change occurs. Notifying Listeners is O(N). With our approach, the only Listener being notified is the InheritedNotifier therefore, our notifications are actually O(1). Accessing an InheritedNotifier from a descendant is O(1) as well, so as you can see, our approach is certainly performant. Let’s take a look at how to implement it.

Implementation

First, let’s make our MyInheritedNotifier class. It is extremely simple, doing nothing but pass its constructor parameters to its super class, InheritedWidget, and define the Listenable type that it will accept.

We are using generics to make our solution adaptable & reusable. If you are unfamiliar with generics, they allow us to code classes and functions to work with a range of data types. Here, we are defining MyInheritedNotifier to work with ChangeNotifier and any subclass of it with <T extends ChangeNotifier>. Anytime we use T below that, we are stating that it will be whatever type T represents. When using this in our code we could specify the type to expect, such as MyInheritedNotifier<AppState>, but by defining notifier to be of type T, it is actually implied when calling the constructor. Therefore, we will automatically be type-safe when using our solution.

Now, let’s wrap our MyInheritedNotifier in a StatefulWidget. This is where our data will be stored. It must be stateful because Listenables should be disposed.

Here, we have simply provided a widget that will store our app state data object. We use a create function to instantiate the object because this class is responsible for disposing the object. Therefore, it should be the one to create it as well. The late keyword allows us to access the widget at instantiation. The line late final T _notifier = widget.create() is equivalent to

That’s it for our InheritedNotifier setup! Wasn’t so bad right? In ~30 lines of code, we have created a performant & reusable state management solution! To use it, we simply define a class that extends ChangeNotifier and defines our app state.

As an example, I will define an AppState class for the typical Flutter counter app. I will not only store the count, but also a background and text color that will change along with count.

As stated earlier, the convention is to provide a static of function that accesses the InheritedWidget. Here, we do so in our app state data class. It finds the correct InheritedNotifier via BuildContext's dependOnInheritedWidgetOfExactType method. We check that there actually is a MyInheritedNotifier in higher up in our widget tree. If not, we probably forgot to create it, or have not passed it to the current context. Then, we return the AppState notifier from MyInheritedNotifier.

We can then use it in our app to access the variables and methods within.

And there you have it! As you can see, everything behaves as expected. We can expand on AppState class to handle any number of states, for example authentication, or we can nest our Providers to provide scoped state management, dividing various states into their own class (e.g. Counter, Auth, Layout, etc) with nothing more than a new data class for each.

I don’t know about you, but I don’t want to write an of method for every data class. Nor do I enjoy accessing my data by calling of over and over again as my project grows. Time for a Flutter pro move…

Extension Methods

Introduced in Dart 2.7, extension methods allow us to provide new functionality to existing classes, and it couldn’t be easier. Instead of passing the BuildContext to a static of method, let’s add an extension on BuildContext to handle this for us. We can use generics to make it reusable for any data type that we want to access. Define an extension in the same library as our classes.

Now, instead of final appState = AppState.of(context);, we can call context.watch<AppState>();. This also allows us to make MyInheritedNotifier private by renaming it to _MyInheritedNotifier, preventing it from being used outside of the library. Beautiful! But what if we want to access AppState somewhere in our app just once, without rebuilding every time it is updated. Perhaps in an onPressed function. We can by adding the following function to our extension:

Here, instead of depending on the MyInheritedNotifier meaning we want to rebuild when the data changes, we are simply looking it up one time. Perfect! Three classes, an extension, and only 40 lines later, we’re done!

Conclusion

Great job! We have coded a pretty elegant state management solution that is concise, adaptable, & performant. It is great for a monolithic app state class, smaller scoped state classes, and (my personal favorite) handling user authentication. Try it out in your next project!

While it’s pretty simple to copy/paste this short amount of code into every project, why not create a package to use? Better yet, clone the simple_state_management repo. It includes some nice additions, such as lazy loading of data objects & helpful documentation. Other features are planned in future updates as well, such as a MultiProvider class to prevent the need to nest Providers that share the same scope. Don’t forget to star the project if you find it beneficial! 😃

As always, thanks for reading! Claps are appreciated, as are questions & comments. Please follow to keep up with my Flutter focused articles, including my Flutter Case Study series, where we examine issues experienced by other Flutter developers. Case Study suggestions are welcome!

--

--

Lee Phillips
Geek Culture

Software engineer. Flutter fanatic. I am here to share knowledge & inspire others to create great experiences.