Update 2019.7.8: Fixed the bug about data initialization and ensuring that the build function has no side effects. If there are any problems with the article, please contact me to fix it.

2020.2.1 Update: Provider 3.1 added Selector. For details, see Step 4: Obtaining state in Subpages.

Update 2020.2.1 Added migration guide for Provider 4.0. Refer to “Migrating Provider 3.X to 4.0” at the end of the article.

preface

Google I/O 2019, The Provider, written by community author Remi Rousselet and the Flutter Team, was officially introduced at the Pragmatic State Management in Flutter (Google I/O’19) keynote Replace Provide as one of the officially recommended state management methods.

As longtime readers may know, in a previous article I introduced a state management feature called Provide under Google’s official repository. At first glance these two things might easily be mistaken for the same thing, but on closer inspection, there is just one word difference. ๐Ÿง

First, the biggest difference you need to know is that Provide is eliminated by Provider… If you are the lucky goose with the Provide, your heart should have begun to rain * this is not pit dad ๐Ÿคฆโ™€๏ธ. I would like to apologize to this part of the friends, after all, many of them just read my previous article into the pit. Fortunately, it’s not too difficult to migrate from Provide to Provider.

This article is based on the latest Provider V-3.0. In addition to explaining how to use it, I think it is more important to explain the application scenarios and usage principles of different “provide” methods of Provider. And principles to follow when using state management to lighten your thinking load while writing the Flutter App. I hope this article can bring you some valuable reference. (This article is a long one, so stay and read it.)

Recommended reading time: 1 hour

What’s the problem

Before formally introducing providers, let me say a few more words about why we need state management. If this is clear to you, it is advisable to skip this section.

If our application was simple enough, With Flutter as a declarative framework, you might just need to map data to views. You probably don’t need state management, as follows.

But as you add functionality, your application will have dozens or even hundreds of states. This is what your application should look like.

WTF, what the hell is this. It’s hard to test and maintain our state clearly because it seems so complicated! There are also multiple pages that share the same status. For example, when you enter a “like” post and exit to the external thumbnail display, the external thumbnail display also needs to show the number of likes, so you need to synchronize the two states.

Flutter actually provides us with a way to manage our state from the very beginning: the StatefulWidget. But we soon found out that it was the culprit in the first place.

While you can use callback when State belongs to a particular Widget and communicating between widgets, when nesting is deep enough, we add an awful lot of junk code.

At this point, we urgently needed a framework to help us clarify these relationships, and a state management framework came into being.

What is Provider

So how do we solve this bad situation? After getting used to the library I can say that providers are a pretty good solution. (you said ๐Ÿ˜’ last time about Provide.) Let’s start with a brief overview of what providers do.

Provider is easy to understand from the name, it is used to provide data, whether on a single page or in the entire app has its own solution, we can easily manage state.

It’s a lot of abstraction, but let’s do the simplest example.

How to do

Here we still use the Counter App as an example to introduce how to share the state of Counter in two independent pages. It looks like this.

The two page center fonts share the same font size. The button on the second page will increase the number, and the number on the first page will increase synchronously.

Step 1: Add dependencies

Add the Provider dependency in pubspec.yaml.

  • For details, see pub.dev/packages/pr…
  • For details, see juejin.cn/post/684490…

Step 2: Create the data Model

The Model here is really our state, which not only stores our data Model, but also contains methods to change the data and expose the data it wants to expose.

import 'package:flutter/material.dart';

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get value => _count;

  voidincrement() { _count++; notifyListeners(); }}Copy the code

The intent of this class is very clear. Our data is an int _count, underlined to indicate private. Expose the _count value by getting value. The increment method is provided to change the data.

There is a mixin mixed with a ChangeNotifier, a class that helps us automatically manage our audience. When a notifyListeners() is called, it notifies all listeners to refresh.

If you are a mixin this concept is not clear, can see my translation before the Dart translation ใ€‘ ใ€ | what is mixins.

Step 3: Create top-level shared data

We initialize the global data in the main method.

void main() {
  final counter = CounterModel();
  final textSize = 48;

  runApp(
    Provider<int>.value(
      value: textSize,
      child: ChangeNotifierProvider.value(
        value: counter,
        child: MyApp(),
      ),
    ),
  );
}
Copy the code

Provider

. Value can manage a constant data and provide it to descendant nodes. We simply declare the data in its value property. So here we pass in the textSize.

The ChangeNotifierProvider

. Value not only provides data for descendant nodes to use, but also notifies all listeners when data changes. (Through the notifyListeners we’ve talked about before)

The

paradigm here can be omitted. But I recommend declaring it anyway, it will make your application more robust.

In addition to the above properties, Provider

. Value provides the UpdateShouldNotify Function to control the refresh timing.

typedef UpdateShouldNotify<T> = bool Function(T previous, T current);

We can pass a method here (T previous, T current){… }, and get the first and last two Model instances, then compare the two models to define the refresh rule, return bool indicates whether refresh is required. Default to Previous! = current Refreshes.

Of course, there is a key property, a general operation. If you still don’t know, it is suggested that reading this article before I [Flutter | covering the Key] (juejin. Cn/post / 684490…). .

To keep you thinking straight, I’ll put up the bland MyApp Widget code here. ๐Ÿ˜‘

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( theme: ThemeData.dark(), home: FirstScreen(), ); }}Copy the code

Step 4: Get the state in the child page

Here we have two pages, FirstScreen and SecondScreen. Let’s look at the code for FirstScreen.

Provider.of(context)

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _counter = Provider.of<CounterModel>(context);
    final textSize = Provider.of<int>(context).toDouble();

    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Text(
          'Value: ${_counter.value}', style: TextStyle(fontSize: textSize), ), ), floatingActionButton: FloatingActionButton( onPressed: () => Navigator.of(context) .push(MaterialPageRoute(builder: (context) => SecondPage())), child: Icon(Icons.navigate_next), ), ); }}Copy the code

The easiest way to get top-level data is provider.of

(context); The

paradigm here specifies that we get FirstScreen to look up to the nearest ancestor node that stores T.

We use this method to get the top-level CounterModel and textSize. And used in the Text component.

FloatingActionButton is used to click to jump to the SecondScreen page, regardless of our theme.

Consumer

If you look at this, you might be thinking, well, both pages get the top-level state, but the code is the same. ๐Ÿคจ Don’t skip to the next section, we’ll look at another way to get status that will affect your app performance.

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Consumer2<CounterModel,int>(
        builder: (context, CounterModel counter, int textSize, _) => Center(
              child: Text(
                'Value: ${counter.value}', style: TextStyle( fontSize: textSize.toDouble(), ), ), ), ), floatingActionButton: Consumer<CounterModel>( builder: (context, CounterModel counter, child) => FloatingActionButton( onPressed: counter.increment, child: child, ), child: Icon(Icons.add), ), ); }}Copy the code

Here we introduce the second method, which uses Consumer to get data from the ancestor node.

We use the common Model in two places on this page.

  • Apply central Text: Use CounterModel to display Text in Text and define its size by textSize. A total of two models were used.
  • Float button: using CounterModelincrementMethod raises the value of the counter. A Model is used.

Single Model Consumer

Let’s first look at floatingActionButton, which uses a Consumer.

The Consumer uses the Builder mode and builds from Builder when it receives an update notification. Consumer

represents which ancestor Model it wants to fetch.

The Consumer Builder is really just a Function that takes three arguments (BuildContext Context, T model, Widget Child).

  • Context: context is the build method incoming BuildContext here did not elaborate, if interested can look at this article before I Flutter | understand BuildContext.
  • T: T is also very simple, which is the data model obtained from the last ancestor node.
  • Child: This is used to build parts that are not related to the Model. Child does not rebuild over multiple runs of The Builder.

It then returns a Widget mapped by these three parameters to build itself.

In the floating button example, we get the top-level CounterModel instance through Consumer. And calls its increment method in the callback of the floating button onTap.

And we successfully extracted the invariant part of Consumer, the Icon in the center of the float button, and passed it into the Builder method as the Child argument.

Consumer2

Now let’s look at the text part of the center. The Text component needs not only the CounterModel to display the counter, but also the textSize to adjust the font size.

In this case you can use Consumer2

. The usage is basically the same as Consumer

, except that the paradigm is changed to two, and the Builder method is also changed to Function(BuildContext Context, A value, B value2, Widget Child).

,b>

Damn it If I want to get 100 models, wouldn’t I have to get a Consumer100? Black question mark. JPG)

However, there is no ๐Ÿ˜.

From the source, you can see that the author only got Consumer6 for us. emmmmm….. To ask for more is to be self-reliant.

I have fixed a cashier error for the author.

The difference between

Let’s look at the internal implementation of Consumer.

@override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }
Copy the code

As you can see, Consumer is implemented through provider. of

(context). But provider. of

(context) is much simpler than Consumer, so why should I make it so complicated?

In fact, Consumer is very useful, and its classic feature is its ability to dramatically narrow your control refresh range in complex projects. Provider.of

(context) listeners the context in which the method was called and notifyListeners of its refresh.

For example, our FirstScreen uses provider.of

(context) to fetch data, while SecondScreen does not.

  • You add one to the build method in FirstScreenprint('first screen rebuild');
  • Then add one to the Build method in SecondScreenprint('second screen rebuild');
  • Click the float button on the second page, and you’ll see this output on the console.

first screen rebuild

First, this proves that provider.of

(context) causes a refresh of the context page scope of the call.

What about the second page refresh? Yes, but only the Consumer part was refreshed, and even the Icon in the floating button was not refreshed. You can verify this in the Consumer Builder method without further ado

Suppose you use Provider. Of

(context) in your app’s page-level Widget. It’s easy to see what happens when you refresh the entire page every time its state changes. Although you have the automatic optimization algorithms of Flutter on your side, you certainly won’t get the best performance.

So I recommend that you use Consumer instead of Provider. Of

(context) to get top-level data.

This is the simplest example of using a Provider.

Selector

If you use a Provider in a real project, it’s easy to run into a situation like this. If providers are divided by business, one Provider may provide different data for multiple controls. For example, when we provide Goods as a Provider in e-commerce applications, the server usually returns a List Json of Goods, and we need to provide the entire GoodsList data for display. At this time, if we have a collection attribute, each commodity can be collected separately. If left unprocessed, all Goods dependent on the Provider will be refreshed, which are notifyListeners of the whole list. These are not the desired results.

So it’s natural to wonder, can we filter out useless updates?

To address this problem, Provider version 3.1 introduced the Selector Widget to further enhance the function of Consumer.

Here is a simple example of implementing a list of items.

class GoodsListProvider with ChangeNotifier {
  List<Goods> _goodsList =
      List.generate(10, (index) => Goods(false.'Goods No. $index'));

  get goodsList => _goodsList;
  get total => _goodsList.length;

  collect(int index) {
    var good = _goodsList[index];
    _goodsList[index] = Goods(!good.isCollection, good.goodsName);
    notifyListeners();
  }
}
Copy the code

We simply give the product two attributes, one is isCollection, which indicates whether to collect the product, and the other is goodsName, which indicates the name of the product. We then implemented the GoodsListProvider to provide the data. Collect method can collect/cancel goods in index position.

Now let’s see how you filter refresh timing by Selector.

class GoodsListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => GoodsListProvider(),
      child: null,); }}Copy the code

First, we still need to provide data at the top of the page via the ChangeNotifierProvider. After version 4.0, the property for creating data was changed to Create, and we created the GoodsListProvider in create

And then we can start implementing this page, and obviously to implement this list we need to know how long the list is, and this total is applied to the whole list, but we don’t want it to refresh because an item in the list changes, So we’re now implementing a “Consumer” that doesn’t refresh through Selector, which filters out all refreshes.

    Selector<GoodsListProvider, GoodsListProvider>(
        shouldRebuild: (pre, next) => false,
        selector: (context, provider) => provider,
        builder: (context, provider, child) {
          return null; },),Copy the code

Selector

just to explain A and S here.
,>

  • A is the type of Provider we get from the top level
  • S is the specific type that we care about, the type that’s really useful to us in the Provider that we get, and we need to return that type in the Selector. The refresh scope of the Selector is also changed from the whole Provider to S.

Here I have both types GoodsListProvider because I want to get the whole Provider.

Then we’ll go through the various properties of this Selector:

  • Selector: is a Function that goes in and passes in the top-level provider that we got, and we return the particular part S that we care about.
  • ShouldRebuild: this attribute stores the filtered value of the selector returned by selector and compares the new S with the cached S to see if the selector needs to be refreshed. Default preview! = next refresh.
  • Builder: This is where the Widget is returned, the second parameter provider, which is the S we just returned in selector.
  • Child: This is used to optimize parts that don’t need to be refreshed.

So in this case, I don’t want this Selector to refresh, because if this Selector refreshes, the whole list refreshes, which is exactly what we want to avoid, so should build, I’m returning false. (This Selector will not rebuild for notify)

Then let’s start building the list section.

    ListView.builder(
        itemCount: provider.total,
        itemBuilder: (context, index) {
        
          return Selector<GoodsListProvider, Goods>(
            selector: (context, provider) => provider.goodsList[index],
            builder: (context, data, child) {
              print(('No.${index + 1} rebuild')); }); });Copy the code

The ListView uses the total in the provider we just acquired to build the list. And then each item in the list we want to refresh based on its state, so we need to use Selector again to get the Good that we really care about.

So we can see that the selector here returns provider.goodslist [index], which is a specific item, so each item only focuses on its own piece of information, so that selector’s refresh scope is that item. Let’s print the rebuild information.

Finally fill in the code of the commodity card.

    ListTile(
        title: Text(data.goodsName),
        trailing: GestureDetector(
          onTap: () => provider.collect(index),
          child: Icon(
              data.isCollection ? Icons.star : Icons.star_border),
        ),
      );
Copy the code

Then we start testing, click the Favorites button to check the rebuild situation.

Performing hot reload... Syncing files to device iPhone Xs Max... . flutter: No.8 rebuild flutter: No.9 rebuild flutter: No.10 rebuild Reloaded 2 of 492 librariesin 446ms.
flutter: No.6 rebuild
flutter: No.1 rebuild
Copy the code

Cool! Now only the Selector that we currently have in our collection is rebuilt, so we don’t have to refresh the entire list.

You can view the Widget’s full code here.

You also need to know

Choose the proper constructor for using Provides

In the example above ๐Ÿ‘†, we chose to use the XProvider

.value constructor to create the provider in the ancestor node. In addition to this approach, we can use the default constructor.

Provider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  }) : this._(
          key: key,
          delegate: BuilderStateDelegate<T>(builder, dispose: dispose),
          updateShouldNotify: null,
          child: child,
        );
Copy the code

We won’t go over the usual key/ Child attributes here. Let’s start with a builder that looks a little more complicated.

ValueBuilder

In contrast to the.value construct, which simply passes in a value, builder requires us to pass in a ValueBuilder. WTF?

typedef ValueBuilder<T> = T Function(BuildContext context);

It’s as simple as passing in a Function and returning data. In the example above, you could substitute it like this.

Provider(
    builder: (context) => textSize,
    ...
)
Copy the code

Because we’re in Builder mode, we need to pass in the context by default, and actually our Model (textSize) doesn’t have anything to do with the context, so you can write it this way.

Provider(
    builder: (_) => textSize,
    ...
)
Copy the code

Disposer

Now that we know builder, what does this Dispose method do? In fact, this is the crux of a Provider.

typedef Disposer<T> = void Function(BuildContext context, T value);

The Dispose attribute needs to Disposer

, which is also a callback.

If you’ve used BLoC before, I’m sure you’ve had a headache. When should I release resources? BloC uses the Observer mode, which is intended to replace the StatefulWidget. However, a large number of streams must be closed after they are used to free up resources.

However, the Stateless Widget doesn’t give us a method like Dispose, which is BLoC’s hard injury. You have to use StatefulWidget in order to release resources, which is against our intent. Provider solves this for us.

When the Provider is removed, it will Disposer

, and you can release resources from this location.

For example, let’s say we have a BLoC like this.

class ValidatorBLoC {
  StreamController<String> _validator = StreamController<String>.broadcast();

  get validator => _validator.stream;

  validateAccount(String text) {
    //Processing verification text ...} dispose() { _validator.close(); }}Copy the code

At this point we want to provide the BLoC on a page but don’t want to use the StatefulWidget. At this point, we can put the Provider on the top of the page.

Provider(
    builder:(_) => ValidatorBLoC(),
    dispose:(_, ValidatorBLoC bloc) => bloc.dispose(),
    }
)
Copy the code

This perfectly solves the data release problem! ๐Ÿคฉ

Now we can safely use it together with BLoC. It’s great, isn’t it? But now you might be wondering which constructor I should choose when using a Provider.

My recommendation is to choose Provider

. Value for the simple model, with the benefit of precise control over refresh timing. The default construct of Provider() is your best choice for complex models that require resources to be freed.

Several other providers follow this pattern, and you can check the source code if you need to.

Which Provider should I use

If you provide a listener (Listenable or Stream) and its subclasses in your Provider, you will get the following exception warning.

You can provide the CounterModel used in this article with a Provider (remember hot restart instead of Hot Reload) and you’ll see the above FlutterError.

You can also disable this prompt in the main method with the following line of code. Provider.debugCheckInvalidValueType = null;

This is because the Provider can only provide constant data and cannot tell dependent child widgets to refresh. If you want to use a Provider that causes change, use the following Provider.

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

You may wonder why (Listenable or Stream) does not work, and why ChangeNotifier is mixed into our CounterModel but FlutterError appears.

class ChangeNotifier implements Listenable

Let’s look at the similarities and differences between these providers. Focus on the ListenableProvider/ChangeNotifierProvider classes.

ListenableProvider provides objects that subclass the Listenable abstract class. Since it cannot be mixed in, the Listenable capability is inherited, and the addListener/removeListener methods must be implemented to manually manage listeners. Obviously, this is so complicated that we usually don’t need to do it.

A ChangeNotifier class automatically helps us manage the audience, so ListenableProvider can also accept ChangeNotifier classes.

The ChangeNotifierProvider is simpler and provides a class that inherits/blends in/implements ChangeNotifier to its child nodes. Usually we just need to create a ChangeNotifier in the Model and call the notifyListeners whenever we need to refresh the state.

What is the difference between ChangeNotifierProvider and ListenableProvider? ListenableProvider can also provide models mixed with ChangeNotifier.

Again, the question you need to think about. Is your Model here a simple Model or a complex Model? This is because ChangeNotifierProvider will automatically call its _Disposer method whenever you need it.

static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier? .dispose();

We can override ChangeNotifier’s Dispose method in the Model to release its resources. This is useful for complex Model situations.

By now you should know the difference between ListenableProvider and ChangeNotifierProvider. Now let’s look at ValueListenableProvider.

ValueListenableProvider is used to provide models that implement inheritance/mixin/ValueListenable. It is actually dedicated to a ChangeNotifier that only has a single change of data.

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>

Classes handled by ValueListenable call notifyListeners when they no longer need data updates.

Well, there’s only one last StreamProvider left.

Streamproviders are specifically used to provide a Single Stream. I’ll cover only the core properties here.

  • T initialDataYou can use this property to declare the initial value of the stream.
  • ErrorBuilder<T> catchError: This property is used to catch errors in the stream. After this stream is addError, you should be able to passT Function(BuildContext context, Object error)Call back to handle the exception data. It is very useful in practical development.
  • updateShouldNotify: As with the previous callback, I won’t repeat it here.

In addition to the properties that all three constructors have, there are three different constructors for StreamProviders.

  • StreamProvider(...)The default constructor is used to create a Stream and listen to it.
  • StreamProvider.controller(...): Create a file in Builder modeStreamController<T>. The StreamController is automatically released when the StreamProvider is removed.
  • StreamProvider.value(...): listens on an existing Stream and provides its value to descendant nodes.

In addition to the five providers already mentioned, there is also a FutureProvider that provides a Future to its descendant nodes and notifies the dependent descendant nodes to refresh when the Future is complete. I won’t go into details here, but check the API documentation if necessary.

Gracefully handle multiple providers

In our previous example, we used a nested approach to combine multiple providers. It looks silly (I just have a hundred models ๐Ÿ™ƒ).

Here we can use a very sweet component called MultiProvider.

Now we can change that example to something like this.

void main() {
  final counter = CounterModel();
  final textSize = 48;

  runApp(
    MultiProvider(
      providers: [
        Provider.value(value: textSize),
        ChangeNotifierProvider.value(value: counter)
      ],
      child: MyApp(),
    ),
  );
}
Copy the code

Our code is suddenly much cleaner, and it’s exactly the same as the nesting we just did.

Tips

Ensure that the build method has no side effects

Build without side effects is also commonly called build keep pure, which means the same thing.

It is common to see the xxx. of(context) method called in the build method to get top-level data. You must be very careful that your build function should not have any side effects, including new objects (other than widgets), requests to the network, or making an operation outside of a mapped view.

This is because you have no control over when your build function will be called. I can say anytime. Every time your build function is called, there is a side effect. This is going to be a very scary thing. ๐Ÿคฏ

It’s a little abstract when I say this, but let’s do an example.

Suppose you have an ArticleModel that takes a page of List data from the web and displays it on the page with a ListView.

At this point, let’s assume you do the following in the build function.

@override
  Widget build(BuildContext context) {
      final articleModel = Provider.of<ArticleModel>(context);
      mainCategoryModel.getPage(); // By requesting data from the server
      returnXWidget(...) ; }Copy the code

We get the articleModel from the ancestor node in the build function, and then call the getPage method.

What happens is that when we get the result of the request successfully, we call provider.of

(context), as we described earlier; Will rerun its build. The getPage is then executed again.

Each request to getPage in your Model increases the number of current request pages stored in the Model (the first request for page 1, the second request for page 2, and so on), so each build will result in a new request for data. And request the data on the next page when the new data gets. It’s only a matter of time before your server goes down. Come on baby!

Since the didChangeDependence method is also called as dependencies change, it also needs to be guaranteed that it has no side effects. For details, see initialization of single-page data.

So you should strictly follow this principle, otherwise it will lead to a series of bad consequences.

So how to solve the problem of data initialization, see the Q&A section.

Don’t put all states globally

The second tip is not to put all your states at the top. Developers often like to put everything on top of the top-level MaterialApp after getting into state management for convenience. So it looks like it’s easy to share the data, and I want the data and I just get it.

Don’t do it. Strictly distinguish your global data and local data, resources are not used to release! Otherwise, your application performance will be severely affected.

Try to use the private variable “_” in the Model

This is probably the question that all of us have when we are new to the game. Why use private variables? It’s convenient that I can manipulate members from anywhere.

An application requires a large number of developers, and your code may be seen months later by another developer, and if your variables are not protected, it may also be countController.sink.add(++_count), the original method, Instead of calling increment method that you’ve already encapsulated.

The effect of both approaches is exactly the same, but the second approach will have our Business Logic mixed in with the rest of the code. Over time, the project will be filled with a large number of these garbage code increases the project code coupling degree, is not conducive to the maintenance and reading of the code.

So be sure to protect your Model with private variables.

Control your refresh range

The property of combination over inheritance can be seen everywhere in Flutter. Common widgets are actually made up of smaller widgets all the way down to the basic components. Controlling the refresh range of widgets is critical to achieving higher performance in our applications.

As we learned from the previous introduction, the way the Model is retrieved in the Provider affects the refresh scope. So, try to use Consumer to get the ancestor Model to keep the refresh range to a minimum.

Q&A

Here are some answers to some of the most common questions you might have, and if you have any other questions, feel free to discuss them in the comments section below.

How does a Provider share state

This is actually a two-step problem.

Get top-level data

Sharing data in ancestor nodes is something we’ve touched on a number of times in previous articles, all via the system’s InheritedWidget.

Providers are no exception. In all Provider build methods, an InheritedProvider is returned.

class InheritedProvider<T> extends InheritedWidget

Flutter passes information down the Element tree by maintaining an InheritedWidget hash table on each Element. Typically, multiple Elements refer to the same hash table, and that table changes only when Element introduces a new InheritedWidget.

Therefore, the time complexity of finding the ancestor node is O(1) ๐Ÿ˜Ž

Notice the refresh

The notification refresh step is actually described in the various providers, in fact, the Listener mode is used. A bunch of listeners are maintained in the Model, and then notifiedListener notifies refresh. (Space for time ๐Ÿคฃ

Why does the global state need to be on top of the top-level MaterialApp

Need to combine the Navigator and BuildContext to answer this question, the Flutter in the previous article | understand BuildContext already explained, go here.

Where should I initialize the data

The problem of data initialization must be discussed separately.

Global data

The main function is a good choice when we need to fetch the global top-level data (as in the previous CounterApp example) and need to do something that produces additional results.

We can create and initialize the Model in the main method so that it will only be executed once.

Single page

If our data is only needed on this page, then you have two options.

StatefulWidget

Here is a correction of a mistake, thanks to @xiaojie’s V laugh and @fantasy525 for pointing it out to me in the discussion.

In previous versions of this article I recommended data initialization in didChangeDependence for State. This is actually a continuation of the habit of using BLoC. Because InheritWidget is initialized only in the didChangeDependence phase of State, the initState phase cannot make a persistent connection with the data (listen). Since BLoC is a Stream, the data goes directly to the Stream and is listened by the StreamBuilder, so State is always dependent on the Stream object. DidChangeDependence does not trigger again. So what’s different about providers.

  /// If [listen] is `true` (default), later value changes will trigger a new
  /// [State.build] to widgets, and [State.didChangeDependencies] for
  /// [StatefulWidget].
Copy the code

The notes in the source code explain that if provider. of

(context) listen, notifyListeners Will trigger the context of the State of the [State. Build] and [State. DidChangeDependencies] method. That is, if you use data that is not provided by a Provider, such as ChangeNotifierProvider, this will change the dependent class and fetch the data when provider. of

(context, listen: True) If you select LISTEN (the default is LISTEN), the didChangeDependencies and Build methods are re-run when the data is refreshed. This also has a side effect on didChangeDependencies. If data is requested here, when the data arrives, the next request is triggered, and the request continues indefinitely.

In addition to side effects, this error occurs when a data change is a synchronous action, such as the counter.increment method called in didChangeDependencies.

The following assertion was thrown while dispatching notifications for CounterModel:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<CounterModel> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. This exception is
flutter: allowed because the framework builds parent widgets before children, which means a dirty descendant
flutter: will always be built. Otherwise, the framework might not visit this widget during this build phase.
Copy the code

This has to do with the construction algorithm of Flutter. In simple terms, setState() or markNeedsBuild() cannot be called during State’s build. In our case, this method was called during didChangeDependence, resulting in this error. Asynchronous data is not executed immediately due to the Event loop. Students who want to learn more about Flutter technology can read the Widget on Flutter Fast Car.

Feel everywhere pit ah, that how should initialize. The solution I’ve found so far is this: first of all, to make sure that we don’t have side effects from initializing data, we need to find a method that must only run once in the State declaration cycle. InitState is created for this purpose. We are now at the top of the page itself, where the page-level Model is created, and there is no need for Inherit at all.

class _HomeState extends State<Home> {
    final _myModel = MyModel();
    
      @override
  void initState() {
    super.initState(); _myModel.init(); }}Copy the code

Page-level Model data is created and initialized in the top-level Widget of the page.

We also need to consider how the operation should be handled if it is a synchronous operation, as in the case of counterModel.increment.

 void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((callback){
      Provider.of<CounterModel>(context).increment();
    });
  }
Copy the code

We call increment at the end of the first frame build with the addPostFrameCallback callback so that we don’t get a build error.

Provider author Remi offers another approach

This code is relatively unsafe. There’s more than one reason for didChangeDependencies to be called.

You probably want something similar to:

MyCounter counter;

@override
void didChangeDependencies() {
  final counter = Provider.of<MyCounter>(context);
  if(conter ! =this.counter) {
    this.counter = counter; counter.increment(); }}Copy the code

This should trigger increment only once.

That is, determine whether the data already exists before initializing it.

cascade

You can also use the Dart concatenation syntax… Do () is initialized directly when the StatelessWidget member variable is declared on the page.

class FirstScreen extends StatelessWidget { CounterModel _counter = CounterModel().. increment();double _textSize = 48; . }Copy the code

When using this approach, note that the state is lost when the StatelessWidget reruns the build. This can happen during child page switching in a TabBarView.

So we recommend using the first option, which is to initialize data in State.

Do I need to worry about performance

Yes, no matter how much Flutter optimizes and how much Provider consideration there is, there is always a way for the app to freeze ๐Ÿ˜‚ (just kidding)

This only happens when we don’t follow its code of conduct. Performance is going to be terrible because of all the things you do wrong. My advice is to follow its specifications. Do everything that will affect performance, knowing that Flutter optimizes the update algorithm to O(N).

A Provider is simply an upgrade to an InheritedWidget, and you don’t have to worry about introducing a Provider that will cause performance problems for your application.

Why choose Provider

Not only does a Provider provide data, but it has a complete solution that covers most situations you will encounter. It also solved the thorny dispose problem that BLoC had not solved, and the intrusion of the ScopedModel.

But is it perfect? No, at least not yet. The Build pattern of Flutter widgets can easily be componentized at the UI level, but there is still a dependency between models and Views using only providers.

We can only remove dependencies by manually converting a Model to a ViewModel, so if you have componentization requirements, you’ll need to deal with them separately.

But for the most part, providers are good enough to allow you to develop simple, high-performance, hierarchical applications.

How should I choose state management

With all this introduction to state management, you may find that some state managers have no conflicting responsibilities. BLoC, for example, can be combined with the RxDart library to become very powerful and useful. BLoC can also be used in combination with Provider/ScopedModel. Which state management mode should I choose?

My advice is to observe the following:

  1. The point of using state management is to make it easier to write code, so don’t use any state management that complicates your application.
  2. BLoC/Rxdart/Redux/fish-redux are all difficult to handle. Do not choose a state management method that you do not understand.
  3. Before making a final decision, play a demo and really feel the benefits/disadvantages of each state management approach before making your decision.

Hope I can help you.

Source analyses

Here to share a bit of source code analysis (really very shallow ๐Ÿ˜…)

Builder mode in Flutter

In Provider, the original constructor of each Provider takes a builder parameter, which is usually used as (_) => XXXModel(). It’s a bit of a one-size-fits-all, so why not be as concise as the.value() constructor?

In fact, the Provider uses the Delegation Pattern to help us manage the Model.

The ValueBuilder declaration is eventually passed to the agent class BuilderStateDelegate/SingleValueDelegate. Model lifecycle management is then implemented through proxy classes.

class BuilderStateDelegate<T> extends ValueStateDelegate<T> {
  BuilderStateDelegate(this._builder, {Disposer<T> dispose})
      : assert(_builder ! =null),
        _dispose = dispose;
  
  final ValueBuilder<T> _builder;
  final Disposer<T> _dispose;
  
  T _value;
  @override
  T get value => _value;

  @override
  void initDelegate() {
    super.initDelegate();
    _value = _builder(context);
  }

  @override
  void didUpdateDelegate(BuilderStateDelegate<T> old) {
    super.didUpdateDelegate(old);
    _value = old.value;
  }

  @override
  voiddispose() { _dispose? .call(context, value);super.dispose(); }}Copy the code

Here only put BuilderStateDelegate, the rest please check the source code.

How to implement MultiProvider

Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }
Copy the code

The MultiProvider is essentially wrapped layer by layer with the cloneWithChild method that each provider implements.

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)
Copy the code

Is equivalent to

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)
Copy the code

Migrate from Provider 3.X to 4.0

Flutter SDK version

Provider 4.0 requires that the minimum Flutter SDK version be greater than V1.12.1. If your SDK is smaller than this, you will receive this error when obtaining dependencies.

The current Flutter SDK version is xxx

Because provider_example depends on provider >=4.0.0-dev whichRequires Flutter SDK version >=1.12.1, version solving failed. Pub get failed (1) Process finished withexit code 1
Copy the code

So if your project is not based on Flutter V1.12.1 or above, there is no need to upgrade the Provider.

Breaking Changes

builder ๅ’Œ initialBuilderThe name

(May appear in the “provide data” area)

  • initialBuilderrenamedcreate
  • “Proxy” providerbuilderInstead ofupdate
  • The classic of the providerbuilderrenamedcreate

Provide lazy loading

The new CREATE/Update is now lazily loaded, meaning that they are not created when the Provider is created, but when the value is first read.

If you don’t want it to be lazy, you can also turn lazy loading off by declaring lazy: false.

Here is an example:

FutureProvider(
  create: (_) async => doSomeHttpRequest(),
  lazy: false,
  child: ...
)
Copy the code

Interface changes

SingleChildCloneableWidget interface removed and replaced by a new type of control SingleChildWidget.

You can check the details in this issue.

The Selector to enhance

Now the Selector shouldRebuild supports deep comparisons between two lists, so if you don’t want to do that you can pass a custom shouldRebuild.

Selector<Selected, Consumed>(
  shouldRebuild: (previous, next) => previous == next,
  builder: ...,
)
Copy the code

The DelegateWidget series is removed

Now if you want to customize a Provider, you can directly inherit the InheritedProvider or another existing Provider.

New Usage Tips

Provider author Remi has added a number of useful tips for using providers to the Readme.

The sample App

You can check out the Provider sample application on FlutterNotebook.

Write in the last

This time the writing is too smooth, accidentally write too much. Can see here friends, are very strong ๐Ÿคฃ.

This is not so much a Provider session as a summary of my experience in state management. Hope to be able to give you reference value.

Some of the Tips and Q&A in the post are actually applicable to most state management, and I’ll consider a section on them later. However, the topic of the next article has already been decided, implementing context-free navigation in Flutter. Don’t miss it if you are interested.

If you have any questions or suggestions about your Provider, please feel free to contact me in the comments section below or at [email protected], and I will reply as soon as possible!