Geek Culture
Published in

Geek Culture

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 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.

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:

  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.

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 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.

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 Listenables 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 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.

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 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!

--

--

--

A new tech publication by Start it up (https://medium.com/swlh).

Recommended from Medium

Community Update #1 | Mint Recap & What’s Next

.dotenv

📢 Holdex Chain Testnet is coming!

CSS: Day 64

Day 64 in yellow

AXL token? Yes!

Why is security important in infrastructure as code ?

Custom Celery Tasks: including the enqueuing request_id to a task_id in Django

Code Smell 79 — TheResult

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lee Phillips

Lee Phillips

Software developer. Flutter fanatic. Other interests include photography, sports, coffee, and food.

More from Medium

Recipe for a Private Flutter Package

Dynamically Pinned List Headers

Relative vs package imports in Flutter and Dart

Create a real-time chat with pure Flutter and Dart