A Closer Look at the Provider Package

Martin Rybak
Flutter NYC
Published in
10 min readJul 30, 2019

--

Plus a Brief History of State Management in Flutter

Provider is a state management package written by Remi Rousselet that has been recently embraced by Google and the Flutter community. But what is state management? Heck, what is state? Recall that state is simply the data that represents the UI in our app. State management is how we create, access, update, and dispose this data. To better understand the Provider package, let’s look at a brief history of state management options in Flutter.

1. StatefulWidget

A StatelessWidget is a simple UI component that displays only the data it is given. A StatelessWidget has no “memory”; it is created and destroyed as needed. Flutter also comes with a StatefulWidget that does have a memory thanks to its long-lived companion State object. This class comes with a setState() method that, when invoked, triggers the widget to rebuild and display the new state. This is the most basic, out-of-the-box form of state management in Flutter. Here is an example with a button that always shows the last time it was tapped:

class _MyWidgetState extends State<MyWidget> {
DateTime _time = DateTime.now();
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text(_time.toString()),
onPressed: () {
setState(() => _time = DateTime.now());
},
);
}
}

So what’s the problem with this approach? Let’s say that our app has some global state stored in a root StatefulWidget. It contains data that is intended to be used by many different parts of the UI. We share that data by passing it down to every child widget in the form of parameters. And any events that intend to mutate this data are bubbled back up in the form of callbacks. This means a lot of parameters and callbacks being passed through many intermediate widgets, which can get very messy. Even worse, any updates to that root state will trigger a rebuild of the whole widget tree, which is inefficient.

2. InheritedWidget

InheritedWidget is a special kind of widget that lets its descendants access it without having a direct reference. By simply accessing an InheritedWidget, a consuming widget can register to be automatically rebuilt whenever the inherited widget is rebuilt. This technique lets us be more efficient when updating our UI. Instead of rebuilding huge parts of our app in response to a small state change, we can surgically choose to rebuild only specific widgets. You’ve already used InheritedWidget whenever you’ve used MediaQuery.of(context) or Theme.of(context). It’s probably less likely that you’ve ever implemented your own stateful InheritedWidget though. That’s because they are tricky to implement correctly.

3. ScopedModel

ScopedModel is a package created in 2017 by Brian Egan that makes it easier to use an InheritedWidget to store app state. First we have to make a state object that inherits from Model, and then invoke notifyListeners() when its properties change. This is similar to implementing the PropertyChangeListener interface in Java.

class MyModel extends Model {
String _foo;
String get foo => _foo;

void set foo(String value) {
_foo = value;
notifyListeners();
}
}

To expose our state object, we wrap our state object instance in a ScopedModel widget at the root of our app:

ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)

Any descendant widget can now access MyModel by using the ScopedModelDescendant widget. The model instance is passed into the builder parameter:

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<MyModel>(
builder: (context, child, model) => Text(model.foo),
);
}
}

Any descendant widget can also update the model, and it will automatically trigger a rebuild of any ScopedModelDescendants (provided that our model invokes notifyListeners() correctly):

class OtherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text('Update'),
onPressed: () {
final model = ScopedModel.of<MyModel>(context);
model.foo = 'bar';
},
);
}
}

ScopedModel became a popular form of state management in Flutter, but is limited to exposing state objects that extend the Model class and its change notifier pattern.

4. BLoC

At Google I/O ’18, the Business Logic Component (BLoC) pattern was introduced as another pattern for moving state out of widgets. BLoC classes are long-lived, non-UI components that hold onto state and expose it in the form of streams and sinks. By moving state and business logic out of the UI, it allows a widget to be implemented as a simple StatelessWidget and use a StreamBuilder to automatically rebuild. This makes the widget “dumber” and easier to test.

An example of a BLoC class:

class MyBloc {
final _controller = StreamController<MyType>();
Stream<MyType> get stream => _controller.stream;
StreamSink<MyType> get sink => _controller.sink;

myMethod() {
// YOUR CODE
sink.add(foo);
}
dispose() {
_controller.close();
}
}

An example of a widget consuming a BLoC:

@override
Widget build(BuildContext context) {
return StreamBuilder<MyType>(
stream: myBloc.stream,
builder: (context, asyncSnapshot) {
// YOUR CODE
});
}

The trouble with the BLoC pattern is that it is not obvious how to create and destroy BLoC objects. In the example above, how was the myBloc instance created? How do we call dispose() on it? Streams require the use of a StreamController, which must be closed when no longer needed in order to prevent memory leaks. (Dart has no notion of a class destructor; only the StatefulWidget State class has a dispose() method.) Also, it is not clear how to share this BLoC across multiple widgets. So it is often difficult for developers to get started using BLoC. There are some packages that attempt to make this easier.

5. Provider

Provider is a package written in 2018 by Remi Rousselet that is similar to ScopedModel but is not limited to exposing a Model subclass. It too is a wrapper around InheritedWidget, but can expose any kind of state object, including BLoC, streams, futures, and others. Because of its simplicity and flexibility, Google announced at Google I/O ’19 that Provider is now its preferred package for state management. Of course, you can still use others, but if you’re not sure what to use, Google recommends going with Provider.

Provider is built “with widgets, for widgets.” With Provider, we can place any state object into the widget tree and make it accessible from any other (descendant) widget. Provider also helps manage the lifetime of state objects by initializing them with data and cleaning up after them when they are removed from the widget tree. For this reason, Provider can even be used to implement BLoC components, or serve as the basis for other state management solutions! 😲 Or it can be used simply for dependency injection — a fancy term for passing data into widgets in a way that reduces coupling and increases testability. Finally, Provider comes with a set of specialized classes that make it even more user-friendly. We’ll explore each of these in detail.

Installing

First, to use Provider, add the dependency to your pubspec.yaml:

provider: ^3.0.0

Then import the Provider package where needed:

import 'package:provider/provider.dart';

Basic Provider

Let’s create a basic Provider at the root of our app containing an instance of our model:

Provider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)

The builder parameter creates instance of MyModel. If you want to give it an existing instance, use the Provider.value constructor instead.

We can then consume this model instance anywhere in MyAppby using the Consumer widget:

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<MyModel>(
builder: (context, value, child) => Text(value.foo),
);
}
}

In the example above, the MyWidget class obtains the MyModel instance using the Consumer widget. This widget gives us a builder containing our object in the value parameter.

Now, what if we want to update the data in our model? Let’s say that we have another widget where pushing a button should update the foo property:

class OtherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text('Update'),
onPressed: () {
final model = Provider.of<MyModel>(context);
model.foo = 'bar';
},
);
}
}

Note the different syntax for accessing our MyModel instance. This is functionally equivalent to using the Consumer widget. The Consumer widget is useful if you can’t easily get a BuildContext reference in your code.

What do you expect will happen to the original MyWidget we created earlier? Do you think it will now display the new value of bar? Unfortunately, no. It is not possible to listen to changes on plain old Dart objects (at least not without reflection, which is not available in Flutter). That means Provider is not able to “see” that we updated the foo property and tell MyWidget to update in response.

ChangeNotifierProvider

However, there is hope! We can make our MyModel class implement the ChangeNotifier mixin. We need to modify our model implementation slightly by invoking a special notifyListeners() method whenever one of our properties change. This is similar to how ScopedModel works, but it’s nice that we don’t need to inherit from a particular model class. We can just implement the ChangeNotifier mixin. Here’s what that looks like:

class MyModel with ChangeNotifier {
String _foo;
String get foo => _foo;

void set foo(String value) {
_foo = value;
notifyListeners();
}
}

As you can see, we changed our foo property into a getter and setter backed by a private _foo variable. This allows us to “intercept” any changes made to the foo property and tell our listeners that our object changed.

Now, on the Provider side, we can change our implementation to use a different class called ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)

That’s it! Now when our OtherWidget updates the foo property on our MyModel instance, MyWidget will automatically update to reflect that change. Cool huh?

One more thing. You may have noticed in the OtherWidget button handler that we used the following syntax:

final model = Provider.of<MyModel>(context);

By default, this syntax will automatically cause our OtherWidget instance to rebuild whenever MyModel changes. That might not be what we want. After all, OtherWidget just contains a button that doesn’t change based on the value of MyModel at all. To avoid this, we can use the following syntax to access our model without registering for a rebuild:

final model = Provider.of<MyModel>(context, listen: false);

This is another nicety that the Provider package gives us for free.

StreamProvider

At first glance, the StreamProvider seems unnecessary. After all, we can just use a regular StreamBuilder to consume a stream in Flutter. For example, here we listen to the onAuthStateChanged stream provided by FirebaseAuth:

@override
Widget build(BuildContext context {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot snapshot){
...
});
}

To do this with Provider instead, we can expose this stream via a StreamProvider at the root of our app:

StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged,
child: MyApp(...),
}

Then consume it in a child widget like any other Provider:

@override
Widget build(BuildContext context) {
return Consumer<FirebaseUser>(
builder: (context, value, child) => Text(value.displayName),
);
}

Besides making the consuming widget code much cleaner, it also abstracts away the fact that the data is coming from a stream. If we ever decide to change the underlying implementation to a FutureProvider, for instance, it will require no changes to our widget code. In fact, you’ll see that this is the case for all of the different providers below. 😲

FutureProvider

Similar to the example above, FutureProvider is an alternative to using the standard FutureBuilder inside our widgets. Here is an example:

FutureProvider<FirebaseUser>.value(
value: FirebaseAuth.instance.currentUser(),
child: MyApp(...),
);

To consume this value in a child widget, we use the same Consumer implementation used in the StreamProvider example above.

ValueListenableProvider

ValueListenable is a Dart interface implemented by the ValueNotifier class that takes a value and notifies listeners when it changes to another value. We can use it to wrap an integer counter in a simple model class:

class MyModel {
final ValueNotifier<int> counter = ValueNotifier(0);
}

When using complex types, ValueNotifier uses the == operator of the contained object to determine whether the value has changed.

Let’s create a basic Provider to hold our main model, followed by a Consumer and a nested ValueListenableProvider that listens to the counter property:

Provider<MyModel>(
builder: (context) => MyModel(),
child: Consumer<MyModel>(builder: (context, value, child) {
return ValueListenableProvider<int>.value(
value: value.counter,
child: MyApp(...)
}
}
}

Note that the type of the nested provider is int. You might have others. If you have multiple Providers registered for the same type, Provider will return the “closest” one (nearest ancestor).

Here’s how we can listen to the counter property from any descendant widget:

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<int>(
builder: (context, value, child) {
return Text(value.toString());
},
);
}
}

And here is how we can update the counter property from yet another widget. Note that we need to access the original MyModel instance.

class OtherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text('Update'),
onPressed: () {
final model = Provider.of<MyModel>(context);
model.counter.value++;
},
);
}
}

MultiProvider

If we are using many Provider widgets, we may end up with an ugly nested structure at the root of our app:

Provider<Foo>.value( 
value: foo,
child: Provider<Bar>.value(
value: bar,
child: Provider<Baz>.value(
value: baz ,
child: MyApp(...)
)
)
)

MultiProvider lets us declare them all our providers at the same level. This is just syntactic sugar; they are still being nested behind the scenes.

MultiProvider( 
providers: [
Provider<Foo>.value(value: foo),
Provider<Bar>.value(value: bar),
Provider<Baz>.value(value: baz),
],
child: MyApp(...),
)

ProxyProvider

ProxyProvider is an interesting class that was added in the v3 release of the Provider package. This lets us declare Providers that themselves are dependent on up to 6 other Providers. In this example, the Bar class depends on an instance of Foo. This is useful when establishing a root set of services that themselves have dependencies on one another.

MultiProvider ( 
providers: [
Provider<Foo> (
builder: (context) => Foo(),
),
ProxyProvider<Foo, Bar>(
builder: (context, value, previous) => Bar(value),
),
],
child: MyApp(...),
)

The first generic type argument is the type your ProxyProvider depends on, and the second is the type it returns.

Listening to Multiple Providers Simultaneously

What if we want a single widget to list to multiple Providers, and trigger a rebuild whenever any of them change? We can listen to up to 6 Providers at a time using variants of the Consumer widget. We will receive the instances as additional parameters in the builder method.

Consumer2<MyModel, int>(
builder: (context, value, value2, child) {
//value is MyModel
//value2 is int
},
);

Conclusion

By embracing InheritedWidget, Provider gives us a “Fluttery” way of state management. It lets widgets access and listen to state objects in a way that abstracts away the underlying notification mechanism. It helps us manage the lifetimes of state objects by providing hooks to create and dispose them as needed. It can be used for simple dependency injection, or even as the basis for more extensive state management options. Having received Google’s blessing, and with growing support from the Flutter community, it is a safe choice to go with. Give Provider a try today!

Very Good Ventures is the world’s premier Flutter technology studio. We built the first-ever Flutter app in 2017 and have been on the bleeding edge ever since. We offer a full range of services including consultations, full-stack development, team augmentation, and technical oversight. We are always looking for developers and interns, so drop us a line! Tell us more about your experience and ambitions with Flutter.

--

--