Tour of the Provider package, points of interest and to care about.

Introduction

I recently received many questions related to the Provider package, featured at the Google IO’19 that I decided to write this post to answer most of the questions that were raised.

This article tends to tell what the Provider package actually is and is not, without entering into the details on the way of using it, and also puts the focus on things to pay special attention to.

Before providing my own view, let’s consider what the author of that package said himself at the FlutterLDN meeting on 17th June:

  • The Provider package is:

    • “A (set of) good default behavior …” [06:07]
    • “a part of a State Management” [07:27]
  • The Provider is not:

    • “Scoped_Model v2” [06:54]
  • In the future, Provider will evolve to offer bindings with Mobx and maybe with flutter_bloc [24:40]


Part 1: The main role of a provider

What is the main role of a Provider at first?

The initial objective of a Provider is, as its name indicates, to provide something to whoever would need to have access to it.

Therefore a Provider needs to be positioned as an ancestor of a tree of Widgets so that it could provide something to any of the Widgets, part of the tree.


Picture of the Provider

The following diagram shows how the Provider (and its variations) is architectured.

Provider architecture

Having a look at the picture, we can directly spot that the Provider is a StatefulWidget, which will build an InheritedProvider ( = extended InheritedWidget).


Basic Behavior: simple Provider

As previously said, the basic behavior is to provide something to its descendants.

To achieve this, here is a very basic implementation:

Provider<MyClass>(
    builder: (BuildContext context) => MyClass(),
    dispose: (BuildContext context, MyClass value) => value?.dispose(),
    child: Container(),
);

We use the builder to tell the Provider what it needs to provide. The BuildContext corresponds to the one of the Provider.

Personal note

The term “builder” is a bit misleading as it is usually reserved for building some widget(s) structure. Something like “oneTimeValueBuilder” could have been more appropriate as it is called only once at the initState() of the BuilderStateDelegate.

The “dispose” method allows taking the decision on what needs to be done when this Provider is itself disposed.

For any descendant, to get access to the “MyClass”, it can be done via:

final MyClass myClass = Provider.of<MyClass>(context);

As simple as this…

Warning: optional parameter “listen”

However, you need to be aware that the helper method “Provider.of<T>(…)” has an optional boolean parameter, called “listen” (the default value is true). So the previous code could also have been written as follows:

final MyClass myClass = Provider.of<MyClass>(context, listen: true);

Depending on both the nature of the data you are handling with the Provider and the case, there will be circumstances where you will have to set this optional parameter to false in order to prevent unnecessary rebuildings… but I will come back on this parameter a bit later.


What makes Provider different from the “usual” other providers?

To that stage, the Provider’s basic behavior is not very different from any other Provider. The only visible differences are:

  • externalization of

    • the delivery of the data to be “provided” by the Provider (via the builder),
    • the disposal
    • (not always available) the decision whether to notify the InheritedWidget’s dependent contexts (via the updateShouldNotify)
  • provision of the initial data

    • the builder is called only once during the whole lifecycle of the instance of the Provider

What is the biggest advantage of the Provider, compared to many other ones?

Both the externalization of the provision of the value to be provided and the conditional notification are, according to me, the biggest added values of the Provider, compared to other Providers (such as my BlocProvider or others).

As the Provider is in total control of the moment when to ask for the provision of the value to be provided, it ensures that that value is not asked twice. In other words, not instantiated twice. This is thanks to the externalization of its parameter (builder) which is called at its internal implementation of a StatefulWidget initState() method.


Is Provider a State Management System?

It depends on what you understand with State Management System.

If your understanding of a State Management System is to have one place to keep a state (or a value, a model…) and make it available to whoever needs to access it, yes this can also be considered as a State Management System but then a basic Singleton class may also be considered as a State Management solution.

Therefore, I agree with the package author: the Provider is not a State Management System by itself but could be considered as a part of a State Management System.


Part 2: The Notifier part

So why is the Provider different?

Simply because the Provider does not limit itself to providing. The Provider is also a kind of Notifier.

Personal note

Maybe another name could have been more appropriate such as “ProviderNotifier”, for example.

As you can see in the documentation, the Provider comes with many different variations:

  • ListenableProvider
  • ChangeNotifierProvider
  • ChangeNotifierProxyProvider
  • ValueListenableProvider
  • StreamProvider
  • FutureProvider

and with a convenient wrapper Widget:

  • Consumer

For each of these variations of the Provider, when the “provided value” changes, you are able to notify the Widgets part of the tree, rooted by the Provider, under the following conditions:

  1. The “provided class” requests to notify (via the notifyListeners()) or is a Stream or a Future that completes
  2. The optional external method updateShouldNotify returns true (or is absent)
  3. You have a Consumer Widget or Widgets which called the Provider.of<…>(context, listen: true) (with the optional parameter listen == true).

How can we be notified?

The Provider package offers 2 ways of being notified:

  1. Provider.of(context, [listen = true])

    When a Widget registers itself as a dependency of the Provider’s InheritedWidget, that widget will be rebuilt each time a variation in the “provided data” occurs (more precisely when the notifyListeners() is called or when a StreamProvider’s stream emits new data or when a FutureProvider’s future completes).

  2. Consumer Widget

    As the Consumer Widget is a convenient widget that wraps the Provider.of() static method, it almost acts as described here above and will be rebuilt when necessary.

Big difference when using the Consumer Widget

As it is the Consumer that invokes the Provider.of(context) static method, only the Consumer will rebuild (of course if the parent of the Consumer did not itself register as a dependency of the Provider’s internal InheritedWidget)


Is the Provider another version of Scoped Model?

Even if the author says it is not, the ChangeNotifierProvider and ListenableProvider could easily be compared to some alternative to Scoped Model.


Is the Provider going to replace BLoC?

Definitely NO. Provider could be used as a new version of the BlocProvider but that’s all.

Of course, if your BLoC exposes ONLY ONE stream, you might maybe consider replacing your BLoC by a StreamProvider.


Can I use the Provider with the notion of BLoC?

Of course YES, you can use the Provider with the BLoC and it is really straightforward.

The following code shows how to “provide” the BLoC:

class MyWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        return Provider<MyBloc>(
            builder: (BuildContext context){
                return MyBloc();
            },
            dispose: (BuildContext context, MyBloc myBloc){
                myBloc.dispose();
            },
            // child: ...
        );
    }
}

The following code shows how to “use” the BLoC:

class AnotherWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        return StreamBuilder<String>(
            stream: Provider.of<MyBloc>(context, listen: false),
            initialValue: "a value",
            builder: (BuildContext context, AsyncSnapshot<String> snapshot){
                // ...
            }
        );
    }
}

Interesting remark.

As you can see in the code here above, I put the optional parameter “listen” to false. Is it necessary?

In this particular implementation, it is not necessary as inside the build method of this Widget there is no other possible trigger to force the rebuild, however, there is also no need of registering this context as a dependency of the InheritedWidget. This explains why I set listen to false.


Part 3: Parts to pay attention to

The following section draws the attention to some notions that could be of interest…


May we use the Provider inside an initState()?

In other words, may we write something like the following, inside an initState()

MyClass myClass;

@override
void initState(){
    super.initState();
    myClass = Provider.of<MyClass>(context);
}

The answer is NO but if you change your code to the following one, it will work:

MyClass myClass;

@override
void initState(){
    super.initState();
    myClass = Provider.of<MyClass>(context, listen: false);
}

The only difference resides in the use of the optional listen parameter.

If you set this parameter to false (opposite of the default value), yes you can use it inside an initState(), otherwise, you may not and you will have to use it inside the didChangeDependencies() method, for example.

Why does it make a difference?

Simply because, when the listen == true, the of method will invoke the “context.inheritFromWidgetOfExactType” method which is not accepted inside an initState(), while if the listen == false, the of method will invoke the “context.ancestorInheritedElementForWidgetOfExactType” method which can be used inside an initState().


Unnecessary rebuilds

The default value of the listen optional parameter is set to true. This means that each time you are invoking the Provider.of(context) without mentioning the ‘listen’ parameter, you are registering the BuildContext of the Widget to the list of ones that depend on the Provider’s InheritedWidget.

This might seem to be irrelevant but sometimes, it is not.

Let’s take as an example, a Button that needs to access a Model (let’s call it “Counter”) to increase a counter. The Counter model has been provided via the ChangeNotifierProvider.

class Counter with ChangeNotifier {
    int _count = 0;
    int get count => _count;

    void increment(){
        _count++;
        notifyListeners();
    }
}

class ButtonToIncrease extends StatelessWidget {

    @override
    Widget build(BuildContext context){
        Counter counter = Provider.of<Counter>(context);
        return RaisedButton(
            onPressed: (){
                counter.increment();
            },
            child: Text('Press to increase the value'),
        );
    }
}

Because of the listen optional parameter being left to the default value, each time the Counter model will call its notifyListeners() method, this button will be rebuilt.

Therefore, when you simply need to get access to a model to use its methods, do not forget to set listen: false to prevent from rebuilding…


Caution use of the StreamProvider

Case 1: Retrieval of the Stream or StreamController

This section focuses on the StreamProvider and in particular on the fact that either the StreamController (case of use of StreamProvider.controller) or the Stream (case of use of StreamProvider or StreamProvider.value) need to be defined outside the scope of the StreamProvider if you want to be able to use them to emit data at a later stage.

To understand this statement, let’s consider the case of a Stream (of integers) which would have been “created” at the level of the StreamProvider, as follows:

StreamProvider<int>.value(
    value: StreamController<int>.broadcast().stream,
    initialData: 0,
)

Now suppose that you have a Consumer that listens to the <int> variations.

Consumer<int>(
    builder: (_, int value, __) {
        // do something
    },
)

The Consumer will invoke its builder each time the stream listened to by the StreamProvider will emit a value but… how to we get access to the sink of the corresponding StreamController?

You could answer to this question, saying, by using a StreamProvider.controller, for example…

StreamProvider<int>.controller(
    builder: (_) => StreamController<int>.broadcast(),
    initialData: 0,
)

…and then to get the StreamController, do something like:

StreamSink<int> sink = Provider.of<StreamController<int>>(context).sink;

…but this does not work as the Provider.of is not capable of retrieving with 100% guarantee the correct StreamController.

Hence, there is no other way than having the Stream, defined and accessible, from outside the scope of the StreamProvider.


Case 2: Be careful with the data types, being emitted by the Streams

Suppose that you need to have 2 Streams of integers, you could consider the following:

MultiProvider(
    providers: [
        StreamProvider<int>.value(
            value: stream1.stream,
        ),

        StreamProvider<int>.value(
            value: stream2.stream,
        ),
    ],
),

In this example, I defined the StreamProviders at the same moment but nothing prevents you from using StreamProvider(s) at different stages but part of the same Widget tree.

Then, how could you listen to the highest StreamProvider in the widget’s hierarchy (here: stream1.stream)?

It is not possible. The Provider.of<int>(context) will systematically refer to the closest one (based on the context, used in the Provider.of<int>(context).


Therefore, will you ask me, is there any advantage of using a StreamProvider?

Yes, there are advantages and I will illustrate this with the following practical use-case.

Consider a Widget which is responsible for redirecting the user to a login page or to the home page, based on its authentication state. At the same time, I want this widget to only be notified when there is a change is that authentication state.

To illustrate this I will consider a Stream of FirebaseUser (case of Firebase authentication, for example).

class Application extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        return StreamProvider<FirebaseUser>.value(
            value: FirebaseAuth.instance.onAuthStateChanged,
            updateShouldNotify: (FirebaseUser previous, FirebaseUser current)
                => (current != previous),
            child: MaterialApp(
                //...
            ),
        );
    }    
}

class RedirectionWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        return Consumer<FirebaseUser>(
            builder: (BuildContext context, FirebaseUser firebaseUser, Widget child){
                if (firebaseUser == null){
                    _redirectToLogin();
                } else {
                    _redirectToHome();
                }
                return Container();
            }
        );
    }
}

This RedirectionWidget will invoke the _redirectToLogin() when the user is either not authenticated or logs out.

The _redirectToHome() will be called when the user logs in or changes.

Thanks to the updateShouldNotify, we ensure that the Consumer will rebuild only when the FirebaseUser will vary.


Conclusions

I hope that I answered most of the questions that were raised in relation with the Provider package.

Stay tuned for new articles and meanwhile, I wish you a happy coding.