Provider, as the state management framework appointed by Google, has become the first choice for most small and medium-sized apps because of its simple and easy to use. Provider is very simple to use, the official document is not long, basically half an hour to get started. However, it is not easy to use the Provider well, which is related to the operation efficiency and fluency of the App. Here are some Tips for using the Provider to help you squeeze the last bit of performance out of your Flutter!

⚠️ Tip: This is not an introduction to Provider. You need to have a basic understanding of Provider. Beginners are advised to jump to the end of the question and first read the official documents & example teaching.

Update to the latest version

There is no doubt that Flutter, along with the entire third-party plugin community, is iterating at a high level. The Provider is now iterating at 4.0 as a library that has only been out for a little over a year. Each update is not only a Bug fix, but also a number of feature improvements and performance optimizations. For example, the introduction of selectors in 3.1, and the late introduction of performance hints, etc.

Initialize the Provider correctly

All providers have two constructors, a default constructor and a convenience constructor. Many people simply think that a convenience constructor is simply a simpler construct that takes the same parameters. That’s not true. Let’s take ChangeNotifierProvider as an example:

// ✅ Default constructor ChangeNotifierProvider(create: (_) => MyModel(), child:...)Copy the code
// ❌ default constructor MyModel MyModel; ChangeNotifierProvider( create: (_) => myModel, child: ... )Copy the code
// ✅ convenient constructor MyModel MyModel; ChangeNotifierProvider.value( value: myModel, child: ... )Copy the code
/ / ❌ convenience constructor ChangeNotifierProvider. Value (value: MyModel (), the child:...).Copy the code

In simple terms, if you need to initialize a new Value, use the default constructor and pass it through the return Value of the create method. If you already have an instance of this Value, use the convenience constructor to assign the Value directly to the Value argument. The specific reason can refer to this solution.

Use StatelessWidget instead of StatefulWidget whenever possible

With the introduction of providers for unified state management, most widgets no longer need to inherit from StatefulWidgets to update their data. The Maintenance cost of the StatelessWidget is lower than that of the StatefulWidget, and the construction efficiency is higher. At the same time, less code will allow us to control the reconstruction scope more easily, improving rendering efficiency.

Of course, you’ll continue to use StatefulWidget for parts of the logic that need to be attached to the Widget lifecycle, such as the first entry to a page for an HTTP request.

Try to use Consumer instead of Provider.of(context)

There are two types of Provider Value. One is provider.of (context), which directly returns Value.

Since it is a method and cannot be called directly in the Widget tree, we usually put it in the build method before the return method.

Widget build(BuildContext context) {
  final text = Provider.of<String>(context);
  return Container(child: Text(text));
}
Copy the code

However, since the Provider listens for changes in Value and updates the entire context, the cost of updating the Widget returned by the build method can be very high if it is too large or complex. So how can we further control the scope of the Widget updates?

One approach is to wrap the Widget that really needs to be updated into a separate Widget and put the value method inside that Widget.

Widget build(BuildContext context) {
  return Container(child: MyText());
}

class MyText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final text = Provider.of<String>(context);
    returnText(text); }}Copy the code

Another relatively good approach is to use the Builder method to create a narrower context.

Widget build(BuildContext context) {
  return Container(child: Builder(builder: (context) {
    final text = Provider.of<String>(context);
    return Text(text);
  }));
}
Copy the code

Both methods allow you to skip the Container and directly rebuild the Text when you refresh the Widget. Either way, the fundamental goal is to narrow the scope of the context in Provider.of(Context) and reduce the number of Widget reconstructs. But both methods are too cumbersome.

Consumer is another value for Provier, but it is a Widget that can be easily embedded in the Widget tree, similar to the Builder scheme above.

Widget build(BuildContext context) {
  return Container(child: Consumer<String>(
    builder: (context, text, child) => Text(text),
  ));
}
Copy the code

Consumer can directly get the context and Value passed to the Builder as parameters, which is undoubtedly more convenient and intuitive to use, greatly reducing the cost of developers to control the refresh range.

The Container Builder method has a Child property. We can write widgets that are not affected by Value in the Container hierarchy to the child. This way the Widget in the Child is not rebuilt when the Value is updated, further improving efficiency.

Widget build(BuildContext context) {
 return Container(child: Consumer<String>(
   builder: (context, text, child) => Row(
     children: <Widget>[
       Text(text),
       child
     ],
   ),
   child: Text("Unchanging content"))); }Copy the code

The above code puts text, which is not controlled by text, into the Child and into the Builder method so that the text in the child is not rebuilt when the text changes.

Try to use selectors instead of consumers

Selectors were introduced in 3.1 to further control the scope of Widget updates and minimize the scope of listening for refreshes. In actual projects, we often design the Provider’s Value according to business scenarios or page elements. In this case, the Value is actually ViewModel. The consequence of putting a large amount of data into a Value is that a change in one Value triggers the notifyListeners of the entire ViewModel, which in turn triggers a refresh of the entire ViewModel associated Widget.

Therefore, we need a capability that gives us a chance to determine whether or not a refresh is needed before performing a refresh to avoid unwanted flushs. And that ability, that ability, is implemented by selectors.

Selector<ViewModel, String>( selector: (context, viewModel) => viewModel.title, shouldRebuild: (pre, next) => pre ! = next, builder: (context, title, child) => Text(title) );Copy the code

The Selector takes two generic arguments, the Value type of the Provider and the specific parameter types used in the Value. It takes three arguments:

  • Selector: It’s a Function, passing in a Value, and asking us to return the property that’s used in the Value.
  • ShouldRebuild: This Function will pass in two values, one of which is the old value held before, and the new value returned by the selector this time. This is how we control whether we need to refresh the Widget in the Builder. If you do not implement shouldRebuild, the default is a deep comparison between pre and next. If not, return true.
  • Builder: Where the Widget is returned, and the second argument, title, is the String that we just returned in the selector.

So with Selector, we can avoid the embarrassment of one person in the ViewModel changing the whole family’s update. But the use of selectors goes far beyond ViewModel heavy values. Even on a single piece of data, selectors can squeeze as much performance as possible.

For example, if we change one item in a List of data, we tend to update the listTiles in the entire ListView.

return ListView.builder(itemBuilder: (context, index) {
    final foo = Provider.of<ViewModel>(context).foos[index]
    return ListTile(title: Text(foo.didSelected),);
});
Copy the code

If we look at the Performance or Log, we can see that changing the didSelected property of only one of the foos will rebuild all the Listtiles. This is undoubtedly unnecessary.

return ListView.builder(itemBuilder: (context, index) {
  returnSelector< ViewModel, Foo>( selector: (context, viewModel) => viewModel.foos[index], shouldRebuild: (pre, next) => pre ! = next, // this line can omit builder: (context, foo, child) {returnListTile( title: Text(foo.didSelected), ); }); });Copy the code

Using selectors not only makes it easy to get a Value during Widget building, but it also gives us an extra opportunity to decide if we need to rebuild the sub-widget before building it. This way, the ListView will only refactor the modified ListTile each time.

Make good use of the hidden listen property of Provider.of(context)

The previous Consumer seems to replace all scenarios of Provider.of, so do we still need provider.of? We often have such a requirement that we only need to obtain the Value of the upper-layer Provider without listening and refreshing data, such as calling the Value method.

Button(
  onPressed: () =>
      Provider.of<ViewModel>(context).run(),
)
Copy the code

This will cause an error because the onPressed method just needs to get the ViewModel to call the run method, and its internals don’t care if the ViewModel has changed or needs to be refreshed. By default, provider. of listens for changes to the ViewModel and affects performance. The Provider.of(context) method has a hidden property listen. For cases where you don’t care if the Value changes and only need a Value, just set LISTEN to false (the default is true). The Value returned by provider. of will no longer trigger a listener refresh.

Button(
  onPressed: () =>
      Provider.of<ViewModel>(context, listen: false).run(),
)
Copy the code

Avoid getting Value in the wrong place

As mentioned earlier, some logic must depend on the Widget’s life cycle, such as accessing the Provider when entering the page. So many people put their logic in initState or didChangeDependencies of the StatefulWidget.

initState() {
  super.initState();
  print(Provider.of<Foo>(context).value);
}
Copy the code

But doing so is contradictory, and will also report an error. Since you put the load method in the initState callback, that means you want the method to go only once in the Widget’s lifetime, which means that Value here doesn’t care if the Value changes or not.

So, if you just want to get the Value without listening, just use the above listen parameter to turn off the listening.

initState() {
  super.initState();
  print(Provider.of<Foo>(context, listen: false).value);
}
Copy the code

If you want to keep listening for Value and responding to it, then you shouldn’t put your logic in initState. DidChangeDependencies is more appropriate for that logic. But since didChangeDependencies is called many times, you need to check if the Value has changed, so that the didChangeDependencies method does not loop around forever.

Value value;

didChangeDependencies() {
  super.didChangeDependencies();
  final value = Provider.of<Foo>(context).value;
  if(value ! = this.value) { this.value = value;print(value); }}Copy the code

But!

The above scheme is only used to access the Value, and an error will be reported if the Value needs to be changed and an update is triggered (for example, to access the network). Since you cannot trigger state updates (including setState) in initState didChangeDependencies, you may cause the Widgets state to be updated before the last build is complete, resulting in inconsistent state.

Therefore, the official recommendation is to execute the Provider Value method when Value is initialized if it does not depend on external parameters.

class MyApi with ChangeNotifier {
  MyApi() {
    load();
  }

  Future<void> load() async {}
}
Copy the code

If the Provider Value method must rely on external parameters provided by Widgets, you can use future. microTask to package the calling procedure in an asynchronous method. Asynchronous methods are delayed until the next cycle due to the Event loop, avoiding collisions.

initState() {
  super.initState();
  Future.microtask(() =>
    Provider.of<MyApi>(context, listen: false).load(page: page); ) ; }Copy the code

Timely Release of resources

Timely release of resources that are no longer used is the focus of optimization. Provider provides two solutions for timely release of resources.

  1. The Provider’s default constructor has a Dispose callback that is triggered when the Provider is destroyed. We just need to free up our resources in this callback.
Provider(create (_) => Model(), Dispose (context, value) {// Release resources})Copy the code
  1. Override the ChangeNotifier dispose method. You may notice that the Initialization method of ChangeNotifierProvider does not have the dispose parameter. This is because ChangeNotifierProvider automatically calls the Dispose method of Value when it is destroyed. All we need to do is override the dispose method of Value.
class Model with ChangeNotifier { 
  @override
  void dispose() {// Release resources super.Dispose (); }}Copy the code

In fact, this is the biggest difference between ChangeNotifierProvider and ListenableProvider. ChangeNotifierProvider inherits from ListenableProvider, but ChangeNotifierProvider requires a higher type of Value. Dispose is a Method of ChangeNotifier.

In addition, we should avoid placing all Provider states at the top level. Although it is easy to access, none of the global Provider resources can be released, which has a greater impact on performance. We should clarify the business when building new pages and new functions, let the Provider only cover the scope it is responsible for, and release resources in time after exiting the functional page.

Run more Log than Performance

The simplest and most brainless approach is to insert logs between widgets to observe the refresh range of the widgets, and try to find optimization points if the refresh range is too large and doesn’t match the actual logic. Although this kind of investigation method is relatively extensive, it has a significant effect on the project that has not been optimized.

For projects that have done preliminary optimization and want to take the Flutter a step further, the Performance bottleneck can only be analyzed using the Performance matching tool.

conclusion

All of the Tips above are actually doing the same thing: reducing Widget rebuilds.

Although we know that Flutter does a lot of efficient algorithms and strategies internally to avoid ineffective reconstructions and renderings, even efficient algorithms have a cost, and algorithms are a black box for us, we can’t guarantee that they will work all the time, so we need to eliminate useless Widget reconstructions at the source.

Finally, a condensed version of this advice:

  1. Every time I use the Provider to set values, I ask myself if I need to listen for data or just access Value.
  2. Every time you use Provider, you ask yourself if you can use Selector instead of Consumer, and if not, if you can use Consumer instead of Provider. Of (context).

provider

Flutter | state management guide – the Provider