Understanding Provider in Diagrams — Part 3: Architecture

Joseph T. Lapp
Oct 6, 2019 · 10 min read

This article is the third in three-part series that describes the architecture of the Flutter provider package and illustrates this architecture in diagrams. It assumes the reader has read the first two articles, Part 1 and Part 2. This third article summarizes the provider architecture and ties up loose ends.

We are at the end of the series. We have finished exploring the foundations of the provider package, and there’s only a little left for us to do. First, we succinctly summarize the provider architecture so that we have it all in one place. Then we enumerate additional features of the provider package that may be helpful. Finally, we briefly examine why one would use providers instead of just using the InheritedWidget on which they are based.

The Provider Architecture, Annotated

The previous two articles piecewise explained everything in the architecture diagram. It’s time to put it all together. We present the entire architecture and annotate it with information that should be review. For your convenience, here is the architecture diagram again as originally presented:

The architecture has the following structural features:

  • A provider widget makes a value of a particular type available to its descendants. The type is designated here by T. The descendants of a provider that access the value are the provider’s “dependents.”

Components message each other in the following order, with step numbers corresponding to the numbers in the diagram:

  1. Flutter performs the initial build of each dependent widget, prior to any state changes.

Steps 5 through 11 occur when all of the following conditions are met: (a) the provider has a state source; (b) the provider has a dependent that is listening for state changes; and (c ) a state change occurs. Steps 5 and 6 occur on each state change. Step 7 occurs on each state change when there are dependents. Steps 8 through 11 occur at most once every 1/60th of a second, after a state change, no matter how many state changes occurred during this time.

Of course, the UML doesn’t capture all the details. For example, it leaves out how a dependent can use the Consumer class to listen for state changes without directly calling the Provider.of method. This diagram, once understood, mainly conveys the various ways to use providers.

Dependency Injection and Data Binding

Let’s now look at the bigger picture. Sometimes we understand things better by their abstractions. At an abstract level, we can think of the provider architecture as implementing both dependency injection and data binding.

In this architecture, one object (the provider widget) supplies the dependencies (the values) on which another object (the dependent widget) relies, giving them to the other object. That’s dependency injection. We can say that provider widgets inject values into their dependant widgets.

As we’ve seen, the injected value is sometimes the state source, such as when it is a Listenable or a ChangeNotifier. In this case, the value and state source behave as a model. The model might be an AuthenticationService, a User, or a NewsFeed, for example. When a provider injects a model into its dependents, it’s implementing conventional dependency injection.

However, the value is often not the state source, such as when the state source is a Stream, a Future, or a ValueListenable. In this case, the provider designates the state source but gives its dependents the emitted state values. Here, the provider only effectively injects its dependents with a state source, while actually injecting the state values as they change instead.

There’s another way to look at this. A data source (the state source) in one place maintains a value (the state), while dependents in another place use the value, and yet the dependents rebuild when the value changes. This is data binding — the dependents are bound to the value. In other words, the provider binds its dependents to the state of a state source. From this perspective, we can think of the state source as a bindable value, and we can think of the provider as injecting this bindable value into its dependents.

The provider architecture therefore implements both dependency injection and data binding, using data binding for state management.

Additional Features

We’ve examined the provider package’s primary features in depth, but the package offers additional features that help with dependency injection and state management. We introduce them here without going into detail. Refer to the documentation for more information.

MultiProvider

MultiProvider is a convenience class for inserting a series of providers into the widget tree all in a single widget. Instead of writing the following:

Provider<User>(
builder: (_) => User(),
child: Provider<NewsFeed>(
builder: (_) => NewsFeed(),
child: Provider<Friends>(
builder: (_) => Friends(),
child: ...
)
)
)

One can write the following equivalent code:

MultiProvider(
providers: [
Provider<User>(builder: (_) => User()),
Provider<NewsFeed>(builder: (_) => NewsFeed()),
Provider<Friends>(builder: (_) => Friends()),
],
child: ...
)

Multi-State Consumers

There are also convenience classes for dependents that need to access the values of multiple providers. Each class name takes the form ConsumerN, where N is the number of values on which the consumer depends. For example, instead of writing the following:

Consumer<User>(
builder: (context, user, child) {
return Consumer<NewsFeed>(
builder: (context, newsFeed, child) {
return Consumer<Friends>(
builder: (context, friends, child) {
// Do something with the acquired instances
// of User, NewsFeed, and Friends.
},
);
},
);
},
)

One can write the following equivalent code:

Consumer3<User, NewsFeed, Friends>(
builder: (context, user, newsFeed, friends, child) {
// Do something with the acquired instances
// of User, NewsFeed, and Friends.
},
)

Proxy Providers

There are convenience classes for composing provider values in series to make compound values available to descendants. These are the proxy providers. For example, instead of writing the following:

Provider<CloudApi>(
builder: (_) => CloudApi(),
child: Provider<Storage>(
builder: (context) => Storage(Provider.of<CloudApi>(context)),
child: Provider<Recorder>(
builder: (_) => Recorder(Provider.of<Storage>(context)),
child: ...
)
)
)

One can write the following equivalent code, which allows dependents to request any of the types CloudApi, AudioStorage, and Recorder:

MultiProvider(
providers: [
Provider<CloudApi>(builder: (_) => CloudApi()),
ProxyProvider<CloudApi, Storage>(
builder: (_, cloudApi, __) => Storage(cloudApi),
),
ProxyProvider<Storage, Recorder>(
builder: (_, storage, __) => Recorder(storage),
),
],
child: ...
)

This latter code has the advantage of not having to nest for each wrapping of one class by another.

A ProxyProvider delivers the supplied object as a value to its dependents, but proxy classes also exist for treating the supplied object as a state source. For example, ChangeNotifierProxyProvider treats the supplied object as a ChangeNotifier and subscribes to it for state change notifications.

Selectors

A Selector is a dependent widget that controls whether it rebuilds on state change. It is useful when the provided value is a model. A Selector rebuilds only when a particular property of the model changes. The selector function parameter “selects” the property on which the widget depends.

For example, suppose the state source is the following User model:

class User with ChangeNotifier {
String id;
int score;

void changeId(String newId) {
id = newId;
notifyListeners();
}
void changeScore(int newScore) {
score = newScore;
notifyListeners();
}
}

The user interface may frequently display user IDs but not often user scores. These user ID displays should not rebuild every time a user score changes. A dependent can restrict rebuilds to changes in user ID as follows:

Selector<User, String>(
selector: (_, user, __) => user.id,
builder: (_, id, __) => Text(id),
}

Provider vs. InheritedWidget

We’ve repeatedly mentioned how providers defer functionality to Flutter’s InheritedWidget class. If we can do all this with InheritedWidget, why bother using providers? We answer this question in the final section of this series. As we’ll see, less code is required when using providers, usually far less.

Suppose the provider provides an unchanging value, not representing state, such as an object for the currently logged-in user. The widget tree builds once with this user and need not rebuild unless the provider itself rebuilds. Here is code for this, implemented with the provider package on the left and with InheritedWidget on the right:

Injecting values with provider (left) and InheritedWidget (right)

The main difference is that using InheritedWidget entails the additional work of subclassing InheritedWidget and calling ancestorWidgetOfExactType() on the BuildContext in each of the dependents. The code is otherwise nearly identical, with the InheritedWidget code slightly longer. For a closer look, you’ll find the provider code here and the InheritedWidget code here.

This example isn’t entirely realistic because the provider provides a global variable to dependents that the dependents already have access to. In practice, this particular usage of a provider is most helpful for making state values of a StatefulWidget available to descendent widgets. When the StatefulWidget rebuilds on state change, the provider rebuilds with the new state value and provides that state value to its dependents as constant.

Now let’s suppose state does change during the life of the provider and that some descendant widgets must rebuild while others should not. We compare code lengths for implementations of the Counter app presented at the beginning of this series. The counter text rebuilds on state change but the button for incrementing the counter does not. The provider implementation is on the left, and the InheritedWidget implementation is on the right:

The increment app with provider (left) and InheritedWidget (right)

In this case, a provider reduces the amount of code we need to write by quite a bit. Because the provider classes are parameterized, we can name the value type in the constructor, allowing the provider to supply all the required boilerplate. You’ll find the above provider code here and the InheritedWidget code here, should you want a closer look.

These code comparisons make it look like there is no need to ever use an InheritedWidget. This may be the case for most apps, but providers are just an application of InheritedWidget. An InheritedWidget is more basic and offers more flexibility, although at the cost of additional complexity.

Conclusion and Next Steps

I wrote this series of articles primarily to help myself come to a better understanding of the provider package and how it works with Flutter. While I’m not new to Android development, I am new to Flutter and reactive programming. This series documents the pieces I felt I was missing. I put a little extra effort into it hoping to help other Flutter newbies.

The series examined the foundations of the provider package and provided a bunch of example code, but it was not a tutorial. Your next steps might be to reread the provider documentation and work through some tutorials. I no longer remember which tutorials were most helpful for me, but this one by Martin Rybak was helpful early on, and this one by Dane Mackier was helpful for understanding more advanced use of the package. You’ll want to make frequent reference to Remi Rousselet’s thorough dartdocs as you go.

Acknowledgments

I am grateful to Remi Rousselet, the author of the provider package, for patiently answering my many questions and for enduring my failed early efforts to produce diagrams capturing the provider architecture. I also thank Remi Rousselet, Bruno Leroux, and Brady Trainor for their feedback on earlier drafts of this series. Any issues that remain are entirely my responsibility.


Flutter Community

Articles and Stories from the Flutter Community

Joseph T. Lapp

Written by

Software dev and spider taxonomist. Mostly high performance Java. Now pursuing edtech with Typescript, Node, Flutter. Author of patents, specs, nature stories.

Flutter Community

Articles and Stories from the Flutter Community

More From Medium

More from Flutter Community

More from Flutter Community

How refactoring your Flutter App.

More from Flutter Community

More from Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade