What's new in provider 4.0.0!

A compilation of what's new in provider 4.0.0, how to migrate, and how to use it

Rémi Rousselet

5 minute read

A new milestone has been reached for provider: 4.0.0!

Let’s go through all the new things together.

Index

 

Say bye to builder and initialBuilder!

After being deprecated in the version 3.2.0, the parameters builder and initialBuilder or providers are now definitely removed.

They are replaced by the create and update parameters.
This makes the code a lot more readable and frees the name “builder” for a future feature.

How to migrate

Classical providers

For classical providers, replace builder by create like so.

Before:

Provider(
  builder: (context) => MyModel(),
  child: SomeWidget(),
)

After:

Provider(
  create: (context) => MyModel(),
  child: SomeWidget(),
)

“Proxy” providers:

For proxy-providers, replace builder by update and initialBuilder by create.

Before:

ChangeNotifierProxyProvider<Service, Notifier>(
  initialBuilder: (context) => Notifier(),
  builder: (context, service, notifier) => notifier..service = service,
  child: SomeWidget(),
)

After:

ChangeNotifierProxyProvider<Service, Notifier>(
  create: (context) => Notifier(),
  update: (context, service, notifier) => notifier..service = service,
  child: SomeWidget(),
)

 

 


Lazy loading

Until now, inserting any kind of provider in the widget tree immediatly created and subscribed to the object.

This led to situations where an object is created even if it isn’t immediately needed.
Such a situation could be fairly inefficient if the object makes network requests.

Now, as part of the 4.0.0, providers will, by default, lazily create/subscribe to these objects.

What does this mean?

Say you are wrapping your entire application in a StreamProvider like so:

runApp(
  StreamProvider(
    create: (_) => Connectivity().onConnectivityChanged,
    child: MyApp(),
  ),
);

Until now, even if MyApp didn’t care about the connectivity status until it reached a very specific route, your app still listened to the status.
But that is no longer the case. Now, if you create a provider but don’t use the value immediately, nothing happens.

Note:
The .value constructor of some providers benefits from lazy-loading too.
The provider will not listen to value until you try to read the provider.
This can be useful for providers like StreamProvider when combined with Firebase, as a Firebase Stream cause network requests only if it has a listener.

As such, it is safe to write:

StreamProvider.value(
  value: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(),
)

I want to purposefully pre-load my value, what can I do?

In some situations, you want to purposefully load the state of provider even if you don’t need it yet.

To do so, you have a few solutions:

  • pass lazy: false to a provider:

    FutureProvider(
      lazy: false,
      create: (_) => doSomeHttpRequest(),
    )
    

    This will revert to the old behavior where the resources are created immediately when the provider is inserted in the widget tree.

  • read the value.
    If you want to start the loading of a resource at a specific time, simply accessing it is enough.
    For example in the following code, MyModel starts loading when clicking on the button.

    FutureProvider<MyModel>(
      create: (_) => ...,
      child: Builder(
        builder: (context) {
          return RaisedButton(
            // force the computing of `MyModel` on click
            onPressed: () => context.read<MyModel>(),
            child: Text('start loading'),
          );
        },
      ),
    )
    

 

 


Devtool integration

It is now possible to inspect the state of your providers, by using the Flutter devtool.

For example, if you write:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => Counter()),
    Provider(create: (_) => Person(name: 'Remi', age: 24)),
  ],
  child: MyApp(),
)

Then when you’ll open the devtool, you will see:

Note:
If the provider is lazy-loaded and hasn’t been used yet, then the devtool will instead show <not yet loaded>.

My devtool says “Instance of MyClass”, why?

If you want an object to be nicely readable inside the devtool, your object will need one of these two things:

  • have a custom toString implementation.
    This is the easiest solution, and depending on how the class is implemented (such as when using built_value), it may be done automatically for you.

    class Person {
      Person({this.name});
    
      final String name;
    
      @override
      String toString() {
        return '$runtimeType(name: $name)';
      }
    }
    
  • Make your object mix-in DiagnosticableTreeMixin, and then override debugFillProperties.
    More complex but more effective.
    Using this approach instead of toString gives you the ability to “expand” objects inside the devtool.

    The easiest solution to implement this approach is to use the refactoring tool of your IDE (works with both VS Code and Android Studio):

    debugFillProperties

Selector update

The Selector widget got a few improvements.

Deep collection comparison

First, the result of selector will now be deeply compared for collections.
You can therefore write:

Selector<MyModel, List<Person>>(
  selector: (_, model) => model.persons.where((p) => p.age > 18).toList(),
  builder: (_, adults, __) {
    ...
  }
)

Before, with such code, Selector would rebuild more often than expected, because selector created a new list instance all the time and therefore == would return false.

But now, Selector will understand that the selected value is a collection and cause a rebuild only if their content changed instead.

This applies to List but also Map, Set, and Iterable.

Custom comparison

Secondly, you can now override the comparison.

Maybe deeply comparing collections is too expensive for your use-case, or you need a very specific behavior that doesn’t rely on ==.

For example, if you want to revert to the previous behavior where it didn’t deeply compared collections, you could write:

Selector<MyModel, List<Person>>(
  shouldRebuild: (previous, next) => previous == next,
  selector: (_, model) => model.persons,
  builder: (_, myItem, __) {
    ...
  }
)

Tip:
If you find yourself needing a specific shouldRebuild or selector often, consider subclassing Selector.


That’s it!

I hope you like these changes.

Of course, if you have any suggestion, feel free to raise an issue on https://github.com/rrousselGit/provider.

comments powered by Disqus