At Google I/O 2019, the official Pragmatic State Management in Flutter (Google I/O’19) keynote officially introduced Provider, written by community author Remi Rousselet with the Flutter Team, as the replacement for Provide as one of the one of the officially recommended ways to manage state.
Readers old friends should know, in the previous article I introduced Google official repository under a state management Provide. at first glance these two things may easily be considered to be the same thing, take a closer look, this is not just a difference of one word, what is the difference. 🧐
First of all, one of the biggest differences you need to know is that Provide is being done away with by Provider… If you’re the lucky goose who uses Provide, your heart should have already begun to Gambling* Isn’t this a dud? 🤦♀️. I apologize to those of you who have read my previous post, but fortunately, you’ll have to start with Provide. Luckily though, it’s not too hard for you to migrate from Provide to Provider.
This article is based on the latest Provider v-3.0 to introduce, in addition to explaining how to use it, I think it is more important to Provider different “Provide” way to apply the scene and the principle of use. I think it is more important to explain the scenarios and principles of using Provider in different ways, as well as the principles you need to follow when using state management, which will reduce your thinking burden in the process of writing Flutter Apps. I hope this article can bring you some valuable references. (In advance, a precautionary note, this article is long, it is recommended to live in the horse to see.)
Recommended reading time: 1 hour
What’s the problem
Before I formally introduce Provider, allow me to ramble on a bit more about why we need state management. If you’re already well aware of this, then we recommend skipping this section.
If our application is simple enough, Flutter being a declarative framework, you might just need to map the data to a view. You probably don’t need state management, like the following.
But as functionality increases, your app will have dozens or even hundreds of states. At this point your app should look like this.
WTF, what the heck is this. It’s hard to test maintaining our state any more clearly because it just seems so complicated! And there will be multiple pages sharing the same state, for example when you enter a post to like it and exit to the external thumbnail display, the external needs to show the number of likes as well, and you need to synchronize the two states.
Flutter actually provided us with a way to manage state from the start, the StatefulWidget, but we quickly realized that it was the culprit for the above.
When State belongs to a specific Widget and communicates between multiple Widgets, while you can use callbacks to solve this, we add a lot of horrible garbage code when the nesting is deep enough.
At this point, we urgently need an architecture to help us make sense of these relationships, and the state management framework was born.
What is Provider
So how do we solve the above bad situation. After getting my hands on the library I can say that Provider is a pretty good solution. (You said the same thing last time you introduced Provide 😒) Let’s start with a brief description of what Provider basically does.
Provider is easy to understand from its name, it is used to provide data, whether on a single page or in the whole app has its own solution, we can easily manage the state.
Having said that it’s still abstract, let’s do the simplest example together.
How to do
Here we still use the Counter App as an example to show you how to share the state of the counter in two separate pages, it looks like this.
The center fonts of both pages share the same font size. The buttons on the second page will allow the numbers to increase, and the numbers on the first page will increase at the same time.
Step 1: Add a dependency
Add the Provider dependency to pubspec.yaml.
For actual additions, see: pub.dev/packages/pr…
Failed to add due to version conflict Please refer to: juejin.cn/post/684490…
Step 2: Create Data Model
The Model here is actually 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;
void increment() {
_count++;
notifyListeners();
}
}
The class intent is very clear, our data is an int type _count
with underscores representing private. The _count
value is exposed through get value
. And provide increment
method to change the data.
The mixin is used here to mix in ChangeNotifier
, a class that will help us automate the management of all our listeners. When notifyListeners()
is called, it will notify all listeners to refresh.
If you’re not familiar with the concept of mixin, check out my previous translation of this article [Translation] Dart | What is Mixin.
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(),
),
),
);
}
With Provider<T>.value
it is possible to manage a constant amount of data and make it available to descendant nodes. We just need to declare the data in its value attribute. Here we pass in textSize
.
And ChangeNotifierProvider<T>.value
can not only provide data for use by child nodes, but also notify all listeners to refresh when the data changes. (Via notifyListeners
which we talked about before)
The <T>
paradigm can be omitted here. But I recommend you to declare it anyway, it will make your application more robust.
In addition to the above properties Provider<T>.value
also provides the UpdateShouldNotify
Function to control the refresh timing.
typedef UpdateShouldNotify<T> = bool Function(T previous, T current);
We can pass in a method (T previous, T current){...}
and get the instances of the two Models before and after, and then customize the refresh rule by comparing the two Models and return a bool to indicate whether refresh is needed or not. The default is previous ! = current then refresh.
Of course, the key attribute is definitely there, as usual. If you’re not sure, I recommend reading my previous post [Flutter | Key in depth] ( juejin.cn/post/684490…).
For the sake of coherent thinking, I’ll just put the code for this bland MyApp Widget here. 😑
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: FirstScreen(),
);
}
}
Step 4: Getting the status in a subpage
Here we have two pages, FirstScreen and SecondScreen. let’s start with 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),
),
);
}
}
The easiest way to get the top-level data is Provider.of<T>(context);
where the paradigm <T>
specifies to get the FirstScreen looking upwards to the nearest ancestor node that has T stored in it.
We use this method to get the top-level CounterModel and textSize and use them in the Text component.
The floatingActionButton is used to jump to the SecondScreen page on click, and has nothing to do with our theme.
Consumer
Seeing this you might be thinking, both pages are getting the top level state, isn’t the code the same, so what’s the pinch. 🤨 Don’t jump to the next section, let’s look at another way to get state 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),
),
);
}
}
Here we are going to introduce the second way, using Consumer to get the data in the ancestor node.
In this page, we use the public Model in two places.
Text in the center of the application: using the CounterModel to display the text in the Text, as well as defining its own size through textSize. Two Models are used in total.
Floating Button: Use theincrement
method of the CounterModel to trigger an increase in the value of the counter. A Model is used.
Single Model Consumer
Let’s look at the floatingActionButton first, using a Consumer case.
The Consumer uses the Builder pattern and rebuilds through the builder when it receives an update notification. Consumer<T>
This represents which ancestor of the Model it wants to fetch.
The Consumer’s builder is actually a Function that takes three parameters (BuildContext context, T model, Widget child)
.
context: context is the BuildContext passed in by the build method. I won’t go into detail here, if you are interested, you can read my previous article Flutter | Deeper Understanding of BuildContext.
T: T is also simple, it is the data model in the most recent ancestor node obtained.
child: It is used to build parts that are not related to the Model, and the child is not rebuilt in multiple runs of the builder.
It then returns a widget mapped by these three parameters to build itself.
In this floating button example, we get the top-level CounterModel
instance through Consumer. And we call its increment
method in the callback of the floating button onTap.
And we’ve managed to extract the unchanging part of the Consumer, the Icon
in the center of the floating button, and pass it into the builder method as a child parameter.
Consumer2
Now let’s look at the text part of the center. At this point you may be confused, just we talked about the Consumer to get only a Model, and now the Text component not only need CounterModel to display counters, but also need to get the textSize to adjust the font size.
In this case you can use Consumer2<A,B>
. This is basically the same as Consumer<T>
, except that the paradigm has been changed to two and the builder method has been changed to Function(BuildContext context, A value, B value2, Widget child)
.
Holy crap… If I wanted to get 100 Models, wouldn’t I have to get a Consumer100 (????????????????????????). black question mark.jpg)
However there is no 😏.
As you can see from the source code, the author only got us Consumer6
. emmmmm….. If you want more, you’ll have to do it on your own.
By the way, I helped the author to fix a clerical error.
Let’s look at the internal implementation of Consumer.
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<T>(context),
child,
);
}
As you can see, Consumer
is realized by Provider.of<T>(context)
. But in terms of implementation, Provider.of<T>(context)
is much simpler and better than Consumer
, so why do I have to make it so complicated?
In fact Consumer
is very useful, and is classically able to greatly narrow down the scope of your control refreshes in complex projects. Provider.of<T>(context)
The context that called this method will be treated as a listener and notified of the refresh at notifyListeners
.
As an example, our FirstScreen uses Provider.of<T>(context)
to fetch data, the SecondScreen does not.
You add aprint('first screen rebuild');
Then in the build method in SecondScreen, add aprint('second screen rebuild');
Click on the floating button on the second page, then you will see this output in the console.
first screen rebuild
First of all this proves that Provider.of<T>(context)
causes a refresh of the page scope of the calling context.
What about the second page? Yes, it did, but only the Consumer
part of the page was refreshed, and we even controlled the non-refresh of the Icon
floating button. You can verify this in the builder method of Consumer
, so I won’t bore you with the details here.
Let’s say you use Provider.of<T>(context)
in your application’s page-level widgets. It’s obvious what will happen, you’ll be refreshing the entire page every time its state changes. Although you have Flutter’s auto-optimization algorithm on your side, you’re definitely not getting the best performance.
So here I suggest you try to use Consumer
instead of Provider.of<T>(context)
to get the top level data.
This is the simplest example of using Provider.
Selector
If you use Provider in real projects, it is easy to come across such a situation. If you divide the Provider by business, one Provider may provide different data for multiple controls. For example, in an e-commerce application, we provide goods as a Provider. Usually, the server returns a List Json of Goods, and we need to provide the entire GoodsList for display. If we have a property called Favorites, each item can be individually favorited. At this time, we want to collect a product, after the collection is completed, we will need to notify the interface refresh, then notifyListeners, this time, if you do not add processing, all the Goods dependent on this Provider will be refreshed, that is, the whole list of the scope of the refresh, which is definitely not the result we want.
So it’s natural and easy to wonder if we can still filter out useless updates?
So to address this issue, the Selector Widget has been released in Provider 3.1 to further enhance the functionality of the Consumer.
Here we will simply implement a sample product list.
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();
}
}
We simply give the product two attributes, one is isCollection
which represents whether the product is favorite or not, and the other is goodsName
which represents the product name. Then we implement GoodsListProvider
to provide the data. The collect
method can be used to favorite/unfavorite the product in the index position.
Here’s how to filter the refresh timing by Selector.
class GoodsListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => GoodsListProvider(),
child: null,
);
}
}
First of all, we still need to provide the data at the top level of this page through ChangeNotifierProvider
. After version 4.0, the attribute for creating data has been changed to create, where we create the `GoodsListProvider` that we just built.
Then we can start to implement this page, obviously to implement this list we need to know how long the list is, this total is applied to the whole list, but we don’t want it to refresh because an item in the list has changed, so we are now implementing a “Consumer” that doesn’t refresh via Selector “which filters out all refreshes.
Selector<GoodsListProvider, GoodsListProvider>(
shouldRebuild: (pre, next) => false,
selector: (context, provider) => provider,
builder: (context, provider, child) {
return null;
},
),
Selector<A, S>
Let’s start by explaining the A and S here.
A is the type of Provider we get from the top level
S is the specific type we care about, i.e., the type of the Provider we get that is actually useful to us and needs to be returned in the selector. The refresh range of this selector is also changed from the entire Provider to S.
Here I have both types as GoodsListProvider
because I want to get the whole Provider.
Then we will introduce the properties of the Selector:
selector: is a Function that passes in the top-level provider we’re getting, and we return the part of S that we’re specifically interested in.
shouldRebuild: this property stores the filtered value of the selector, that is, the S returned by the selector, and compares the new S after receiving the notification with the S in the cache to determine whether the selector needs to be rebuilt, by default preview ! = next, then refreshes.
builder: this is where the widget is returned, the second parameter, provider, is the S that we just returned in the selector.- child: this is used to optimize some parts that don’t need to be refreshed.
So here, I don’t want the Selector to be refreshed, because if the Selector is refreshed, the whole list is refreshed, which is what we want to avoid, so I return false for shouldRebuild (the Selector will not be rebuilt by notify).
Then let’s start building this 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'));
},
);
},
);
The ListView here uses the total from the provider we just fetched to build the list. Then for each item in the list we want it to refresh based on its status, so we need to use the Selector again to get the Good we really care about.
We can see that the selector here returns provider.goodsList[index]
, which is a specific item, so each item only focuses on its own part of the information, so the Selector’s refresh range is that item. Let’s print out the rebuild information.
Finally add the code for this merchandise card.
ListTile(
title: Text(data.goodsName),
trailing: GestureDetector(
onTap: () => provider.collect(index),
child: Icon(
data.isCollection ? Icons.star : Icons.star_border),
),
);
Then we’ll start the test by clicking on the Favorites button to see the rebuild.
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 libraries in 446ms.
flutter: No.6 rebuild
flutter: No.1 rebuild
Now only our current favorite Selector is rebuilt, avoiding the need to refresh the entire list.
You can see the full code of the widget here.
You also need to know
Choosing to use Provides’ constructors wisely
In this example above 👆, we chose to use the constructor method of XProvider<T>.value
to create the provider in the ancestor node. In addition to this approach, we can also use the default constructor method.
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,
);
We won’t bother with the usual key/child attributes here. Let’s start with the more complex-looking builder.
ValueBuilder
The builder here requires us to pass in a ValueBuilder as opposed to the .value
construct where passing in a value is ok. WTF?
typedef ValueBuilder<T> = T Function(BuildContext context);
It’s really simple, just pass in a Function to return a piece of data. In the example above, you could replace it with something like this.
Provider(
builder: (context) => textSize,
...
)
Since it’s Builder mode, you need to pass in context by default, and actually our Model (textSize) has nothing to do with context, so you can write it like this.
Provider(
builder: (_) => textSize,
...
)
Disposer
Now that we know about the builder, what does the dispose method do. This is actually the highlight of the Provider.
typedef Disposer<T> = void Function(BuildContext context, T value);
The dispose attribute requires a Disposer<T>
, and this is actually a callback.
If you’ve used BLoC before, I’m sure you’ve run into a headache. When should I release resources? BloC uses the Observer pattern, which is intended to replace StatefulWidgets, however a large number of streams must be closed to free up resources when they are used up.
However, Stateless Widgets don’t give us a method like dispose, which is the hard part of BLoC. You have to use the StatefulWidget in order to dispose of resources, which is not what we intended. Provider solves this for us.
When the node where the Provider is located is removed, it starts Disposer<T>
and then we can release the resources here.
For example, suppose 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();
}
}
If we want to provide this BLoC on a page but don’t want to use a StatefulWidget, we can apply this Provider to the top level of the page.
Provider(
builder:(_) => ValidatorBLoC(),
dispose:(_, ValidatorBLoC bloc) => bloc.dispose(),
}
)
This solves the data release problem perfectly! 🤩
Now we can safely use it with BLoC, it’s awesome, isn’t it? But now you might be wondering, which constructor should I choose when using Provider.
My recommendation is to use Provider<T>.value
for simple models, as it has the advantage of precise control over the refresh timing. For complex models where you need to release resources, Provider()
is definitely the way to go.
Several other Providers follow this pattern, so check the source code if you need to.
Which Provider should I use?
If you provide a listenable object (Listenable or Stream) and its subclasses in the Provider, then you will get the following exception warning.
You can put the CounterModel used in this article into the Provider for provisioning (remember hot restart instead of hot reload) and you’ll see the FlutterError above.
You can also disable this prompt in the main method with the following line of code. Provider.debugCheckInvalidValueType = null;
This is because a Provider can only provide constant data and cannot notify the child parts that depend on it to refresh. The hints also make it clear that if you want to use a Provider that changes, use the following Provider.
- ListenableProvider
- ChangeNotifierProvider
- ValueListenableProvider
- StreamProvider
You may have a question here, not that (Listenable or Stream) can not, why our CounterModel is mixed with ChangeNotifier but still appear this FlutterError it.
class ChangeNotifier implements Listenable
Let’s look at the similarities and differences between the above Providers. Let’s focus on the two classes ListenableProvider / ChangeNotifierProvider
.
ListenableProvider provides (provides) objects that are subclasses that inherit from the Listenable abstract class. Since it is not possible to mix in, the Listenable capabilities are obtained through inheritance, and its addListener / removeListener
method must be implemented to manually manage the listeners. Obviously, this is too complicated and we usually don’t need to do it.
The class mixed with ChangeNotifier
automatically implements listener management for us, so ListenableProvider can also receive the class mixed with ChangeNotifier.
ChangeNotifierProvider is simpler, it can provide a class that inherits/mixes with/implements ChangeNotifier for child nodes. Usually we just need to add with ChangeNotifier
to the Model and call notifyListeners
when we need to refresh the state.
So what is the difference between ChangeNotifierProvider and ListenableProvider, doesn’t ListenableProvider also provide a model mixed with ChangeNotifier.
It’s still the same question that you need to think about. Is the Model you have here a simple model or a complex model. This is because ChangeNotifierProvider automatically calls its _disposer method when you need it.
static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose();
We can override the dispose method of ChangeNotifier in the Model to release its resources. This is useful in the case of complex Models.
By now you should have a pretty good idea of the difference between ListenableProvider / ChangeNotifierProvider
. Let’s look at the ValueListenableProvider.
ValueListenableProvider is used to provide a Model that implements an inherited/mixed in/implemented ValueListenable, which is actually a ChangeNotifier specialized to handle only a single change data.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>
Call notifyListeners
when a class handled by ValueListenable no longer needs data updates.
Well, finally just one last StreamProvider
left.
StreamProvider
It’s used to provide a Single Stream, and I’m only going to explain its core properties here.
T initialData
: You can declare the initial value of this stream with this attribute.
ErrorBuilder<T> catchError
: This property is used to catch errors in the stream. After the stream has addedError, you will be able to handle the exception data with theT Function(BuildContext context, Object error)
callback. This is very useful in practice.
updateShouldNotify
: As with the previous callbacks, I won’t repeat them here.
In addition to the properties that are available in all three constructor methods, StreamProvider has three different constructor methods.
StreamProvider(...)
: The default constructor is used to create a Stream and listen to it.
StreamProvider.controller(...)
: Create aStreamController<T>
via builder. And automatically release the StreamController when the StreamProvider is removed.
StreamProvider.value(...)
: Listens to an existing Stream and provides its value to the descendants.
In addition to the five Providers mentioned above, there is also a FutureProvider, which provides a Future to its descendant nodes and notifies the dependent descendant nodes to refresh when the Future completes, which will not be described in detail here, so check the api documentation if you need it.
Elegant handling of multiple Providers
In our previous example, we used nesting to combine multiple Providers. this looks a bit silly (I just have a hundred Models 🙃 ).
This is where we can use a very sweet component — MultiProvider
.
At this point that example we just had can be changed to look like this.
void main() {
final counter = CounterModel();
final textSize = 48;
runApp(
MultiProvider(
providers: [
Provider.value(value: textSize),
ChangeNotifierProvider.value(value: counter)
],
child: MyApp(),
),
);
}
Our code is instantly much clearer and completely equivalent to the nested approach we just took.
Tips
Ensure that the build method has no side effects
Build without side effects is also commonly called, build keep pure, both mean the same thing.
It is common to see that we call the XXX.of(context) method in the build method in order to get the top-level data. You have to be very careful that your build function doesn’t produce any side effects, including new objects (other than widgets), requesting the network, or doing an action other than mapping a 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, it will have a side effect. And something very scary will happen. 🤯
I’m sure you’ll feel rather abstract when I put it this way, so let’s take an example.
Suppose you have a ArticleModel
The role of this Model is to fetch a page of List data over the network and display it on the page using a ListView.
At this point, let’s assume you’ve done 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
return XWidget(...);
}
We get the articleModel from the ancestor node in the build function and then call the getPage method.
What happens at this point is that when we get the result of our request, the call to Provider.of<T>(context);
re-runs the build, as we’ve already covered. getPage is executed again.
And each request for getPage in your Model will cause the current requested page saved in the Model to increment itself (the first request for the first page of data, the second request for the second page of data, and so on), and then each build will cause a new request for data, and a request for the next page of data at the time of the new data get. It’s only a matter of time before your server hangs. (Come on baby!
Since the didChangeDependence method is also called as the dependency changes, you also need to make sure it has no side effects. See Single Page Data Initialization below for an explanation.
That’s why you should strictly adhere to this principle, otherwise it can lead to a series of bad consequences.
So how to solve this problem of data initialization, please see the Q&A section.
Don’t put all state globally
The second tip is to not put all your state on the top level. Developers who have been introduced to state management often like to put everything on top of the MaterialApp for the sake of convenience and ease of use. This makes it look easy to share data, so if I want data, I can just go get it.
Don’t do it. Strictly distinguish between your global data and local data, and release resources when they are not used! Otherwise, your application performance will be seriously affected.
Try to use the private variable “_” in the Model.
This is probably a question that comes to all of us at the novice stage. Why do I need private variables, isn’t it convenient for me to be able to manipulate members from anywhere.
An application requires a lot of developer involvement, and the code you write may be seen by another developer a few months later, and if your variables are not protected, maybe the same way you make count++, he’ll use a primitive method like countController.sink.add(++_count), instead of calling your encapsulated increment method.
Although the effect of the two ways is exactly the same, but the second way will let our business logic scattered mixed into other code. Over time, the project will be filled with a large number of these garbage code to increase the degree of project code coupling, very detrimental to the maintenance of the code as well as reading.
So, be sure to protect your Model with private variables.
Control your refresh range
In Flutter, combining over inheritance is everywhere. Common widgets are actually combinations of smaller widgets, right down to the base component. In order to make our apps more performant, it’s important to control the refresh range of our widgets.
As we have learned from the previous introduction, the way to get the Model in the Provider will affect the refresh range. So, please try to use Consumer to get the ancestor Model to maintain the minimum refresh range.
Q&A
Here’s an answer to some common questions that you may have, and if you have questions beyond this, feel free to discuss them together in the comments section below.
How the Provider does state sharing
This question actually has two steps.
Getting top-level data
We’ve actually touched on sharing data in ancestor nodes many times in previous articles, all through the system’s InheritedWidget.
Providers are no exception, and an InheritedProvider is returned in the build method of all Providers.
class InheritedProvider<T> extends InheritedWidget
Flutter passes information down the Element tree by maintaining a InheritedWidget
hash table on each Element. Typically, multiple Element references the same hash table, and the table only changes when an Element introduces a new InheritedWidget
.
So the time complexity of finding the ancestor node is O(1) 😎
Notification Refresh
The notification refresh step has actually been covered in the talk about various Providers, but it actually uses the Listener pattern. the Model maintains a bunch of listeners, and then the notifiedListener notifies of the refresh. (Space for Time 🤣)
Why global state needs to be on top of the top-level MaterialApp
This question needs to be answered in the context of Navigator and BuildContext, as explained in the previous article Flutter | Deeper Understanding of BuildContext, so I won’t repeat it here.
Where should I initialize the data
For this issue of data initialization, we have to discuss it in categories.
global data
The main function is a good choice when we need to get global top-level data (as in the previous CounterApp example) and need to do something that will produce additional results.
We can create the Model and do the initialization work in the main method so that it will be executed only once.
If our data just needs to be used in this page, then you have these two options available to you.
StatefulWidget
Here’s a revised error, thanks to @Xiaojie’s V Smile and @fantasy525 for pointing it out for me in the discussion.
In the previous version of this article I recommended that you initialize the data in the State’s didChangeDependence. This is actually a habit carried over from using BLoC. After using InheritWidget, you can only initialize Inherit in didChangeDependence of State, but you can’t establish a persistent connection with the data in initState stage (listen). Since BLoC uses Stream, the data comes in directly through Stream, and StreamBuilder will listen to it, so the dependency of State is always just the Stream object, and the didChangeDependence method will not be triggered again. What is the difference of Provider.
/// If [listen] is `true` (default), later value changes will trigger a new
/// [State.build] to widgets, and [State.didChangeDependencies] for
/// [StatefulWidget].
The comment in the source code explains that if this Provider.of<T>(context)
listened, then when notifyListeners, it will trigger the [State.build] and [State.didChangeDependencies] methods of the State corresponding to the context. In other words, if you use data that is not provided by the Provider, such as ChangeNotifierProvider which changes dependencies, and if you choose listen (the default is listen) when you get the data from Provider.of<T>(context, listen: true)
, then when the data is refreshed, you will re-run the didChangeDependencies and build methods when the data is refreshed. This also has a side effect on didChangeDependencies. If you request data here, when the data arrives, it triggers the next request, and you end up requesting indefinitely.
In addition to the side effects here, if the data change is a synchronized behavior, such as the counter.increment method here, calling it in didChangeDependencies will result in the following error.
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.
This has to do with Flutter’s build algorithm. Simply put, it’s not possible to call setState() or markNeedsBuild() during the build of a state, which is what we’re doing here when we didChangeDependence, resulting in this error. Asynchronous data is not executed immediately due to the event loop. For a more in-depth look, check out this post from Idle Fish Technologies: Flutter Widgets on the Go.
It feels like there are potholes everywhere, so how do we initialize it? Here’s what I’ve found so far. First of all, to make sure that initializing data doesn’t cause side effects, we need to find a method that must only be run once in the State declaration cycle. initState was created for this purpose. We’re at the top of the page itself, and the page-level Model was created at the top, so there’s no need for an Inherit right now.
class _HomeState extends State<Home> {
final _myModel = MyModel();
@override
void initState() {
super.initState();
_myModel.init();
}
}
Page-level Model data is created and initialized in the top-level widget on the page.
We also need to consider the case of how this operation would be handled if it were a synchronous operation, as in the case of the CounterModel.increment operation we cited earlier.
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((callback){
Provider.of<CounterModel>(context).increment();
});
}
We call the increment method at the end of the first frame build via the addPostFrameCallback callback so that there are no build errors.
provider author Remi gives an alternative 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();
}
}
This should trigger increment only once.
That is to say, before initializing the data to determine whether this data already exists.
cascade
You can also initialize StatelessWidget member variables directly on the page when they are declared using dart’s cascading syntax ..do()
.
class FirstScreen extends StatelessWidget {
CounterModel _counter = CounterModel()..increment();
double _textSize = 48;
...
}
With this approach it is important to note that the state will be lost when the StatelessWidget re-runs the build. This can happen when switching between subpages in the TabBarView.
So it is recommended to still use the first one and initialize the data in State.
Do I need to worry about performance?
Yes, no matter how hard Flutter tries to optimize and how many scenarios the Provider takes into account, there’s always a way to get the app stuck 😂 (just kidding)
This happens only when we don’t follow its behavioral norms. Performance can get really bad because of all your misbehavior. My advice is to follow its specification and do everything with performance in mind, knowing that Flutter has optimized the update algorithm to O(N).
The Provider is simply an upgrade to the InheritedWidget, and you don’t have to worry about the performance issues that introducing the Provider will cause to your application.
Why Provider
Not only does Provider provide data, but it has a complete set of solutions for most of the situations you’ll encounter. Even the tricky dispose problem that BLoC didn’t solve, and the intrusiveness of ScopedModel, it solves.
But is it perfect? No, not really, at least for now. The Flutter widget building model is easy to componentize at the UI level, but it’s still easy to create dependencies between the Model and the View just by using the Provider.
We can only remove the dependency by manually converting the Model to a ViewModel, so if you have a need for componentization, you’ll need to deal with that separately.
For the most part, however, Provider is good enough to allow you to develop simple, high-performance, and clearly layered applications.
How should I choose status management
Having introduced so many state management, you may find that some of them do not have conflicting responsibilities with each other. For example, BLoC can be used in conjunction with the RxDart library to become very powerful and useful. And BLoC can also be used with Provider / ScopedModel. So which state management should I choose.
My advice is to observe the following:
The purpose of using state management is to make writing code easier, any state management that adds complexity to your application, don’t use it.
Choose what you can hold, BLoC / Rxdart / Redux / Fish-Redux state management methods are difficult to get started, don’t choose a state management method you can’t understand.
Before you make a final decision, knock on the demo to really get a feel for the benefits/disadvantages of each state management style before you make your decision.
I hope that helps.
source code analysis
Here’s sharing a little source code shallowness (it’s really shallow 😅)
Builder Patterns in Flutter
In Provider, the original constructor of each Provider has a builder parameter, and here we usually just use (_) => XXXModel()
. It feels like a bit of an overkill. Why can’t it be as simple as the .value()
constructor?
In fact, the Provider uses the delegation pattern in order to manage the Model for us.
The ValueBuilder declared by the builder is eventually passed to the proxy class BuilderStateDelegate
/ SingleValueDelegate
. The Model lifecycle management is then realized through the proxy class.
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
void dispose() {
_dispose?.call(context, value);
super.dispose();
}
}
Just put BuilderStateDelegate here, please check the source code for the rest.
How to implement MultiProvider
Widget build(BuildContext context) {
var tree = child;
for (final provider in providers.reversed) {
tree = provider.cloneWithChild(tree);
}
return tree;
}
MultiProvider actually wraps itself layer by layer through the cloneWithChild method that each provider implements.
MultiProvider(
providers:[
AProvider,
BProvider,
CProvider,
],
child: child,
)
AProvider(
child: BProvider(
child: CProvider(
child: child,
),
),
)
Migrating from Provider 3.X to 4.0
Flutter SDK Versions
Provider 4.0 requires a minimum Flutter SDK version greater than v1.12.1, if your SDK is less than this, you will get this error when fetching dependencies.
The current Flutter SDK version is xxx
Because provider_example depends on provider >=4.0.0-dev which requires Flutter SDK version >=1.12.1, version solving failed.
pub get failed (1)
Process finished with exit code 1
So if your project is not based on Flutter v1.12.1 or above, you don’t need to upgrade the Provider.
Breaking Changes
builder
and initialBuilder
renamed
(may appear where “data provided”)
-
initialBuilder
renamedcreate
builder
of the “proxy” provider toupdate
Classic provider’sbuilder
renamedcreate
Provide lazy loading
The new create
/ update
are now lazy loaded, meaning 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 loaded, you can also turn off lazy loading by declaring lazy: false
.
Here’s a sample:
FutureProvider(
create: (_) async => doSomeHttpRequest(),
lazy: false,
child: ...
)
Interface changes
The SingleChildCloneableWidget interface has been removed and replaced by a new type of control, SingleChildWidget.
You can see the details in this issue.
Selector enhancements
Selector’s shouldRebuild now supports deep comparison of two lists, if you don’t want this you can pass a custom shouldRebuild
.
Selector<Selected, Consumed>(
shouldRebuild: (previous, next) => previous == next,
builder: ...,
)
DelegateWidget family removed.
Now if you want to customize the Provider, you can directly inherit from InheritedProvider or other existing Providers.
Add tips for use
Remi, the author of Provider, has added a lot of useful tips for using Provider to the Readme, so I suggest you all check it out.