Simple & Performant State Management without an External Library
Using Flutter’s InheritedNotifier
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 InheritedWidget
s, 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 Stream
s.
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 InheritedWidget
s 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 InheritedWidget
s. These are simply specialized Widget
s 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 InheritedWidget
s, we can lookup the data directly from the 21st Widget
by traversing up the widget tree. However,InheritedWidget
s 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.
NOTE: The conventional method used to provide access to InheritedWidget data throughout the tree is via a static of(BuildContext) method, as seen by Theme.of(context).
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:
- We pass a callback to the
InheritedWidget
that allows it to callsetState
, as well as writing a bunch of extra boilerplate to handle setting/getting of data. An even worse side effect, we must return theInheritedWidget
from our staticof
function rather than the actual data. - We pass the
State
as the data toInheritedWidget
, returning it from theof
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.
NOTE: ValueNotifier is a subclass of ChangeNotifier, so our solution will accommodate both.
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 Listenable
s work is by storing a list of Listener
s and notifying them one-by-one any time a change occurs. Notifying Listener
s 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.
class MyInheritedNotifier<T extends ChangeNotifier>
extends InheritedNotifier<T> {
const MyInheritedNotifier(
{Key? key, required T notifier, required Widget child})
: super(key: key, notifier: notifier, child: child);
}
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 Listenable
s should be disposed.
class MyProvider<T extends ChangeNotifier> extends StatefulWidget {
const MyProvider(
{Key? key, required this.create, required this.child})
: super(key: key); final Widget child;
final T Function() create; @override
State<MyProvider<T>> createState() => _MyProviderState<T>();
}class _MyProviderState<T extends ChangeNotifier> extends
State<MyProvider<T>> { late final T _notifier = widget.create(); @override
void dispose() {
_notifier.dispose();
super.dispose();
} @override
Widget build(BuildContext context) =>
_MyInheritedNotifier<T>(
notifier: _notifier, child: widget.child);
}
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
late final T _notifier;_MyStateProvider() {
_notifier = widget.create();
}
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.
class AppState extends ChangeNotifier { int _count = 0;
int get count => _count; void incrementCounter() {
_count++;
_backgroundColor = _colors[count % 5];
notifyListeners();
} Color _backgroundColor = Colors.blue;
Color get backgroundColor => _backgroundColor; Color get textColor => _backgroundColor == Colors.yellow
? Colors.black87
: Colors.white; static AppState of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<MyInheritedNotifier<AppState>>()!;
assert(result != null, 'No MyInheritedWidget<AppState> in context');
result.notifier!;}
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
.
NOTE: MyProvider, _MyProviderState, and MyInheritedNotifier can be defined in the same file (library) and never touched again once we are done.
We can then use it in our app to access the variables and methods within.
@override
Widget build(BuildContext context) {
final appState = AppState.of(context);
return Scaffold(
AppBar(
title: Text(title,
style: TextStyle(color: appState.textColor)),
backgroundColor: appState.backgroundColor,
),
body: Center(
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(appState.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: appState.incrementCounter,
tooltip: 'Increment',
backgroundColor: appState.backgroundColor,
child: Icon(Icons.add, color: appState.textColor,
),
);
}
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 Provider
s 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.
extension ReadContext on BuildContext {
T watch<T extends ChangeNotifier>() => dependOnInheritedWidgetOfExactType<MyInheritedNotifier<T>>()!.notifier!;
}
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:
T read<T extends ChangeNotifier>() => findAncestorWidgetOfExactType<MyInheritedNotifier<T>>()!.notifier!;
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 Provider
s 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!