Understanding Provider in Diagrams — Part 1: Providing Values

Joseph T. Lapp
Flutter Community
Published in
12 min readOct 2, 2019

This is the first article in a three-part series that describes the architecture of the Flutter provider package and illustrates this architecture in diagrams. The series assumes the reader is familiar with the need for state management as explained in the Flutter docs. This first article explains how provider provides values and rebuilds widgets. See also Part 2: Basic Providers and Part 3: Architecture.

The provider package by Remi Rousselet and the Flutter team is a lightweight but flexible dependency injection and state management tool for Flutter apps. It shares values across widget hierarchies, and it allows widgets to bind themselves to values so that they rebuild as needed to reflect state changes. This series of articles explains how provider works in diagrams and prose.

The Provider Architecture

Let’s start with a UML diagram that presents the provider architecture at a conceptual level. Don’t worry if you don’t know UML, as we’ll step-by-step explain the architecture and this diagram over subsequent sections.

This diagram differs a bit from strict UML to make it more intuitive and more succinct. Arrows indicate messaging between components. Numbers indicate messaging order. For example, the notation “1, 8: build” indicates that the 1st and 8th steps are requests to build. Step sequence 5 through 11 may repeat.

Here are some of the significant features. Don’t worry if you don’t fully understand them yet, as we’ll be explaining every one of them:

  • Providers and their dependents are widgets. Each provider contains a widget tree that includes its dependents.
  • A provider provides values directly to its dependents, on request, preventing values from having to be passed down the call tree.
  • Each value has a type T. Dependents retrieve values by the type of value they need; dependents do not need references to providers.
  • A state source may asynchronously supply state values. Dependents may listen for and rebuild on state changes. Dependents that do not change with changing state need not listen for and rebuild on state changes. State sources include streams, futures, and instances of Listenable.
  • The state source may even be the provided value, as with a Listenable or ChangeNotifier, in which case the value acts as a model. Dependents can access the model, such as to modify it or retrieve state from it.
  • When a state source supplies changing states, it supplies them to the provider rather than to each dependent. By using an InheritedWidget internally, the provider ensures the proper dependents rebuild.

This architecture permits a lot of flexibility. You can choose, as a function of state source, the means by which state changes update dependents. For example, provider includes out-of-the-box support for state sources of type Listenable, ChangeNotifier, Stream, and Future.

Okay, now let’s examine these features by exploring permutations of this architecture. We’ll gradually explain the architecture diagram as we go.

Providing Values

Providers are widgets that make values available to the descendent widgets that need it. These descendant widgets are called “dependents.” A provider is fundamentally a means for sharing a value, as illustrated here:

Each provider has a “child” that is the root of a tree of widgets. The provider makes the value available to all the widgets in this descendant tree. Each descendant individually decides whether to use the value. This is also how Flutter’s InheritedWidget works, with descendants determining the value they need. In fact, a provider uses an InheritedWidget internally to do the job.

There are no restrictions on what a value can be. It is typically a service, a model, or a state. For example, a service might be an API for accessing a local database, messaging a backend server, or performing data transformations. A model might be an object representing a user, an audio recording, or a news feed. A state might be the value of a form field, the amount of data so-far loaded, or the current playing/recording/stopped state of an audio player.

But why provide values at all? Why not just have widgets read global variables or receive values via their constructors? One reason is that the widget tree enforces a strict dependency hierarchy and thus prevents circular dependencies. Another reason is that, were values passed in via constructors, they’d have to be passed down through all the constructors on the intervening widgets, greatly complicating the code. More generally, all the reasons for dependency injection apply.

As we’ll see, providers make accessing values as simple as a widget requesting the value it needs. Moreover, widgets can effectively bind to state values that change over time and rebuild as needed to reflect the changes.

Building the Widget Tree

Let’s get a sense of how a provider widget tree corresponds to code. The following app increments a counter every time the user presses a button. This Counter app is like the default app that the Flutter IDE plugin generates, except that only the Text widget rebuilds when the count changes:¹

You can see all the widgets in one giant nested tree. Notice the provider widget and its two dependent widgets.

Apps are normally much more complex than this. They normally define multiple classes of widgets and construct the widget tree from these classes. Let’s revise this app to reflect the way most apps are structured:²

Notice that upon constructing MyPage, the descendant widgets Scaffold, Consumer, and IncrementButton are not yet constructed. These aren’t constructed until MyPage’s build() is called. But then upon constructing IncrementButton, RaisedButton still hasn’t been constructed. RaisedButton is constructed when IncrementButton's build() is called.

We construct widgets that construct widgets to replace them. The widgets we put in the tree aren’t necessarily the ones being rendered; it’s the ones they ultimately construct that get rendered. We don’t have a complete widget tree until all of the build() methods have been called on all descendant widgets. Once the entire tree is built, it can finally be rendered. In other words:

constructing ≠ building ≠ rendering

The provider widget is constructed before building the dependent widgets it contains. Constructing it first allows it to make its value available to descendants that haven’t been built. When the dependent widgets are later built, they have access to the value and can construct new widgets from it.

It’s possible that a particular build of the widgets in a provider’s widget tree results in no dependents. That’s okay. A provider’s job is simply to make a value available to whichever descendants are built that need the value.

Retrieving Values

A dependent widget is responsible for retrieving the values it needs. Instead of making the request of the BuildContext, as done with InheritedWidget, the request is made of the Provider.of static method. Even so, the mechanism is internally the same. The following is a more detailed depiction of this process:

Each dependent widget requests the value it needs from Provider.of. The Provider.of method retrieves the value from the appropriate provider widget and returns it back to the dependent.

Providers can nest within providers to supply a variety of values to descendent widgets. To get the right value, a dependent must somehow indicate the proper provider when requesting the value. Dependents accomplish this by specifying the type of value they need.

Consider the following widget tree:

Notice that the dependents in the bottom row are each within the scope of two providers. To get the value it needs, a dependent asks Provider.of for the value of a particular type. For example, the dependent at the bottom right asks Provider.of for the value of type Type1, and Provider.of (by way of an InheritedWidget) determines that it needs to retrieve the value of the Provider<Type1> at the top of the diagram.

There is one more potential complication: multiple providers in the tree could each provide values of the same type. To handle this, Provider.of must know where in the tree the requesting dependent resides so that it queries the nearest ancestor provider whose value has that type.

The Provider.of method does the job by taking the type of value needed and the dependent’s build context, and by returning a value of this type:

class Provider<T> {
static T of<T>(BuildContext context, {bool listen = true}) {...}
}

For example, let’s say your tree includes the following provider widget. Here the builder parameter provides the value:

Provider<User>(
builder: (context) => User(), // provided value
child: someWidgetTree
)

Now we can include the following dependent in someWidgetTree:

class MyDependent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return Text("User name: ${user.name}");
}
)

The provider package includes a convenience class, Consumer, that calls Provider.of for us. Using Consumer, we can instead write:

Consumer<User>(
builder: (context, user, child) {
return Text("User name: ${user.name}");
}
)

Architecture for Retrieving Values

Let’s revisit the UML architectural diagram. We have described the portion of the diagram that initially builds dependents, prior to any state changes:

When a provider tree is built and that tree contains one or more dependents, the following steps occur for each dependent. The step numbers correspond to the numbers in the diagram:

  1. The Flutter framework calls the build() method on the dependent to construct the widget to be rendered. (If the dependent takes a builder function, the widget’s build() method calls this function to build the portion of the rendered widget that is tailored to the application.)
  2. During the build, the dependent calls Provider.of<T>() to request a value, where T specifies the type of value it needs. This method locates the provider that provides a value of type T and retrieves its value.
  3. The Provider.of method returns the provider’s value to the requesting dependent.
  4. The dependent finishes building a widget that reflects the acquired value and returns that widget to the Flutter framework to render.

This process suffices for building widgets that depend on unchanging values, such as service APIs. When values represent states or models containing state, the widgets that depend on them need to rebuild on state changes.

Rebuilding on State Change

Some values are unchanging, and the widgets that depend on them don’t rebuild. Other values change over time. We call these latter values “state values” or simply “state.” Widgets that depend on state can rebuild to reflect state changes.

For example, the widget that shows the count in the above counter app needs to change when the count changes. Meanwhile, the increment button in this same app needs to increment the count, thus changing the state, but it doesn’t need to rebuild because its rendering doesn’t change with the count.

The following diagram illustrates this selective rebuilding of descendants:

The provider’s child widget contains many widgets, three of which depend on the state. Two of the dependent widgets rebuild on state changes and a third does not.

Here is the process by which some widgets rebuild and some don’t. The step numbers correspond to the numbers in the above diagram:

  1. Prior to any state changes — when all we have is the initial state — the Flutter framework does its first build of the provider. Building the provider entails building its child, and building the child entails building all the widgets the child contains, including the widgets that depend on state.
  2. The dependent widgets can’t build without the state value, so during their first build, they make their first request for the value. They do this by calling Provider.of<T>() to retrieve the value of type T. The parameters passed (or not passed) to this method whether the dependent widget listens for state changes. In this diagram, only two of the three dependent widgets listen for changes. The third does not need to rebuild on state change.
  3. After the widget tree has been built for the first time based on an initial state, the provider can receive state changes. The provider itself actually rebuilds in response to state changes. Upon rebuilding, it creates a new InheritedWidget from the very same child instance it used initially. Because widgets are immutable, the child is not automatically rebuilt with the provider. Instead, the InheritedWidget identifies the children that are listening to state changes and marks only those for rebuilding.
  4. Flutter re-renders up to 60 times per second. During the next rendering, all of the widgets marked for rebuilding are rebuilt. In this diagram, two of the three dependent widgets rebuild. Note that even if state changes multiple times in the 1/60th-second interval between renderings, the listening widgets are only redrawn once at the end of the interval.
  5. Two of the dependent widgets are now rebuilding for the new state — the two that indicated they were listening to state changes on their previous request for the value. These dependents need the state value again to build again. So once again they call Provider.of<T>() to retrieve the value of type T. However, on this second call, as well as on subsequent calls, it does not matter whether they again ask to listen for state changes, because after the first request to listen, the widget is forever listening.

The difference between rebuilding on state change and not rebuilding on state change is in the parameters handed to Provider.of. Here is this method again. Notice the context and listen parameters:

class Provider<T> {
static T of<T>(BuildContext context, {bool listen = true}) {...}
}

Each widget has its own BuildContext, so the context parameter identifies the widget that is dependent on the requested value. The listen parameter establishes whether this dependent widget listens for and rebuilds on state changes. The method listens by default, so we only need to use the listen parameter to prevent listening to state changes (i.e. listen: false).

Let’s revisit the counter app we presented earlier. The count text rebuilds on each state change, but the increment button does not. The two dependent widgets could call have called Provider.of as follows:

Builder(builder: (context) {
final counter = Provider.of<Counter>(context);
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
),
Builder(builder: (context) {
final counter = Provider.of<Counter>(context, listen: false);
return RaisedButton(
onPressed: () => counter.increment(),
child: Text("Increment"),
);
}),

However, the provider package includes a convenience class for listening to state changes. This is the Consumer class mentioned earlier. In practice, we more commonly write first of the above two widgets as follows:

Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
),

The Consumer class also works with values that aren’t state or that aren’t changing. If the value doesn’t change, its dependent widgets don’t rebuild.

The provider needs to know how to receive state changes to make them available to its dependents. We haven’t yet covered how that happens. You will need to choose a provider class that suits the way state is sourced.

Architecture for Rebuilding on State Change

We’ve now covered the following subset of the architectural diagram, though we’ve assumed the provider somehow receives each new state value:

Let’s quickly review everything. As usual, the step numbers correspond to the numbers in the diagram:

  1. Flutter performs the initial build of each dependent widget, prior to any state changes.
  2. During the build of a dependent widget, the dependent widget calls Provider.of<T>() to request the value it needs, where T is the value’s type. The widget passes listen: false if it does not need to rebuild on state changes; otherwise, the widget listens for and rebuilds on changes.
  3. The call to Provider.of<T>() returns the value (or initial state) of type T.
  4. The dependent widget finishes building a widget that reflects the value (or initial state) and returns that widget to the Flutter framework for rendering.
  5. (We haven’t covered this step yet.)
  6. If the value is dynamically changing state, the provider somehow receives a new state value of type T. We explain this process in the next article.
  7. The provider, via its InheritedWidget, marks each dependent listening to value type T for building. Doing so induces the dependent to rebuild in Flutter’s next rendering frame, which is at most 1/60th of a second later.
  8. During the next rendering frame, Flutter tells the dependent widget to rebuild for the new state value.
  9. During the rebuild of the dependent widget, the dependent again calls Provider.of<T>(), but this time it’s to retrieve the new value. The dependent remains listening for state changes regardless of the value of the listen parameter passed here or subsequently.
  10. The call to Provider.of<T>() returns the latest value of type T.
  11. The dependent widget finishes building a widget that reflects the new value and returns that widget to the Flutter framework for rendering.

To understand the remainder of the diagram, we’ll first need to examine some of the variety of providers available. We dive into that in the next article.

Well done! You’ve survived Part 1 of Understanding Provider in Diagrams. Now you’re ready to read Part 2: Basic Providers, where things get more detailed.

¹ All example code in this series assumes provider version 3. They were built with the dependency provider: ^3.1.0 in the pubspec.yaml.

² We put the provider at the root of the widget tree to ensure that Counter is only constructed once for the app. Place providers in the tree where they have the lifetimes they require.

--

--

Joseph T. Lapp
Flutter Community

Sr. Software Engineer. Full stack TS/Node/Svelte and performant Java. Patents, specs, UML, tutorials. Learning Rust and DDD. JoeLapp.com