The concept of state management does not understand what it means at the beginning. After studying it, we find that state management is actually our original data management

Flutter is declarative, which means that Flutter reflects the current state of the app by updating the UI. Simply put, if we want to update our controls in Flutter, the most basic way is setState().

If we don’t have a lot of components on a page, using setState() is fine, but in practice our page layout is complex enough. One situation is when we are in a page:

If we write all the widgets into one class, it’s easy to get caught up in {{{{}}}} vortex. This is when we think about splitting widgets, but relying on setState() alone would be a pain because setState() is only scoped as a Widget, That is, if you call setState only in the lowest level Widget, it will not update the top level Widget, which means you need to do it through callbacks, and in the process you will find that some of the variables in the Widget class must be immutable, which can cause other complications

As product requirements iterate faster and projects grow larger, we often need to manage the data relationships shared between different components and different pages. When tens or hundreds of data relationships need to be shared, it becomes difficult to maintain a clear flow direction and sequence, leading to a plethora of nested data transfers and callbacks within the application. At this point, we urgently needed a solution to help clarify the relationship between these shared data, so the state management framework came into being.

I met the Provider

Flutter uses a lot of React design ideas in its declarative UI, resulting in state management frameworks such as Flutter_redux, Flutter_mobx, and Fish_redux based on the front-end design concept. However, most of these frameworks are complex and require a certain understanding of framework design concepts, so the learning threshold is relatively high.

The official Flutter state management framework Provider is much simpler. It is not only easy to understand, but also less intrusive, and can easily combine and control UI refresh granularity. As a result, Provider became one of the official recommended ways to manage state at Google I/O 2019.

A Provider is a framework for providing data. It is the syntactic sugar for inheritedWidgets and provides dependency injection capabilities that allow for more flexibility in processing and passing data in the Widget tree.

Before using a Provider, we first need to add the Provider dependency to the pubspec.yaml file:

1 #provider dependencies on a flutter providerCopy the code

1. Simple use

Let’s do a simple example: the first interface reads data, and the second interface reads and writes data

1. Data state encapsulation

With Provider dependencies added, we can encapsulate the data state. Here, we have only one state to share, count. Since the second page also needs to change the state, we also need to include methods to change the data in the data state wrapper:

import 'package:flutter/cupertino.dart'; Class CounterModel with ChangeNotifier {int _count = 0; Int get counter => _count; Void increment() {_count++; notifyListeners(); // Tell listeners to refresh}}Copy the code

We mixed ChangeNotifier with mixins in the resource encapsulation class. This class helps us manage all the listeners that rely on resource encapsulation classes. When a resource wrapper class calls a notifyListeners, it notifies all listeners to refresh.

If the concept is not clear for mixins, can refer to this article the Dart | mixins is what

Try to use private variables in the Model_

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.

2. Create top-level shared data

Now that the resource is packaged, we need to think about where to put it.

Because the Provider is essentially the syntactic sugar of the InheritedWidget, the data passed through the Provider flows from parent to child (or vice versa) in terms of the direction of the data flow. Now we know that we need to put the resource in the parent Widget of FirstProviderPage and SecondProviderPage,

Class MyApp extends StatelessWidget {@override Widget Build (BuildContext Context) {// Encapsulate the data resource return with the Provider component ChangeNotifierProvider. Value (value: CounterModel (), / / need to share data resources child: MaterialApp (home:... )); }}Copy the code

We can inject the data resource dependency into the application by wrapping the Provider directly in the outer layer of the MaterialApp.

We could also write it like this

Void main () {runApp (ChangeNotifierProvider value (value: CounterModel (), / / the child need to share data resources: MyApp ())); }Copy the code

3. Read the data in FirstProviderPage

Class _FirstProviderPageState extends State<FirstProviderPage> {@override Widget build(BuildContext context) {// Fetch the resource final _counter = Provider.of<CounterModel>(context); Return Container(child: Column(children: [// Display the data in the resource Text('Counter: ${_counter.counter}'), // Set the RaisedButton(child: Text(' go to secondary interface '), onPressed: (){ Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondProviderPage())); }) ], ), ); }}Copy the code

4. Write data in SecondProviderPage

class _SecondProviderPageState extends State<SecondProviderPage> { @override Widget build(BuildContext context) { // Final _counter = Provider. Of <CounterModel>(context); Return Scaffold(appBar: appBar (title: Text('SecondProviderPage'),), // Displays the data in the resource body: Container(child: Column(children: [Text('Counter: ${_counter. Counter}'), // Set RaisedButton(onPressed: _counter.increment, child: Icon(Icons.add)) ], ), ), ); }}Copy the code

2, Consumer

Using Provider. Of to obtain resources, you can obtain the read and write interface of the data exposed by resources, and it is relatively simple to realize data sharing and synchronization. However, the side effect of abusing the provider.of method is that when data is updated, other child widgets on the page refresh with it.

We add a widget to the SecondProviderPage

class TestView extends StatelessWidget { @override Widget build(BuildContext context) { print('print TestView build'); return Container(width: 100,height: 100,color: red,); }}Copy the code

Let’s go to ➕

flutter: 0
flutter: print TestView build
flutter: 1
flutter: print TestView build
flutter: 2
flutter: print TestView build
flutter: 3
flutter: print TestView build
flutter: 4
flutter: print TestView build
flutter: 5
flutter: print TestView build
Copy the code

When data is updated, other child widgets on the page refresh with it, which can have a significant impact on performance. So, is there a way to refresh only the resource-dependent widgets when the data resource changes, leaving the other widgets unchanged?

This is where the keyword comes in. Providers have precise control over UI refresh granularity, which is implemented based on Consumer. The Consumer uses the Builder pattern to create the UI, and when notified of the update, builds the Widget from Builder.

Next we modify the SecondProviderPage

class _SecondProviderPageState extends State<SecondProviderPage> { @override Widget build(BuildContext context) { return  Scaffold( appBar: AppBar(title: Text('SecondProviderPage'),), body: Column( children: [// Consumer<CounterModel> = Consumer<CounterModel> (context,CounterModel count,_) => Text('Counter: ${count.counter}'); // Add Consumer<CounterModel>(${count.counter}'); (context, CounterModel counter, child) => RaisedButton( onPressed: counter.increment, child: Icon(Icons.add))), TestView(), ]) ); }}Copy the code

Let’s go to ➕

flutter: 0
flutter: 1
flutter: 2
flutter: 3
flutter: 4
flutter: 5
flutter: 6
flutter: 7
Copy the code

The Builder in Consumer is actually the function that refreshes the UI

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

final Widget Function(BuildContext context, T value, Widget child) builder
Copy the code
  • Context: Context is the BuildContext passed in by the build method

  • 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.

When our Consumer has multiple arguments, we can call different consumers

3, the Selector

In general, a Selector is the same thing as a Consumer, and it gets its data through provider. of, except that a Selector, as its name suggests, filters out unnecessary updates to prevent rebuilds, meaning that it only updates data that meets the criteria.

So let’s look at the definition of Selector:

class Selector<A, S> extends Selector0<S> { /// {@macro provider.selector} Selector({ Key key, @required ValueWidgetBuilder<S> builder, @required S Function(BuildContext, A) selector, ShouldRebuild<S> shouldRebuild, Widget child, }) : assert(selector ! = null), super( key: key, shouldRebuild: shouldRebuild, builder: builder, selector: (context) => selector(context, Provider.of(context)), child: child, ); }Copy the code

First, explain the generics in Selector<A, S> :

  • 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.

Take a quick look at the properties in Selector:

  • Selector: is just a Function that goes in and passes in the top-level provider that we get, and then returns S that we care about.
  • ShouldRebuild: This property 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 rebuilt. Default preview! =next refreshes, and if it’s a collection, performs a deep comparison of selectors.
  • Builder: Just like Consumer, this returns the control to build, and the second argument provider is the S we just returned in selector.
  • Child: This is used to optimize the parts that don’t need to be refreshed, as we said earlier when we said Consumer.

1, simple implementation of a sample list of goods

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(); } } class Goods{ bool isCollection; String goodsName; Goods(this.isCollection,this.goodsName); }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.

2. Filter refresh timing by Selector

class GoodsListScreen extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => GoodsListProvider(), child: , ); }}Copy the code
  • 1. We still need to pass at the top of this pageChangeNotifierProviderProvide data
  • 2. We create the new one in createGoodsListProvider
typedef Create<T> = T Function(BuildContext context);
Copy the code

3. Realize the list interface

And then we can implement the list, and obviously to implement the list we need to know how long the list is, the 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.

    child: Selector<GoodsListProvider, GoodsListProvider>(
        shouldRebuild: (pre, next) => false,
        selector: (context, provider) => provider,
        builder: (context, provider, child) {
          return ListView.builder(
            itemCount: provider.total,
            itemBuilder: (context, index) {}
Copy the code

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 here shouldRebuild, I’m returning false

Create itemView

itemBuilder: (context, index) { return Selector<GoodsListProvider, Goods>( selector: (context, provider) => provider.goodsList[index], builder: (context, data, child) { print(('No.${index + 1} rebuild')); return ListTile( title: Text(data.goodsName), trailing: GestureDetector( onTap: () => provider.collect(index), child: Icon( data.isCollection ? Icons.star : Icons.star_border), ), ); }); },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.

4. Multistate management MultiProvider

We continue with the first example, click on the plus sign until count is plus 1, and the font color of the text changes once

Add a ColorModel

class ColorModel with ChangeNotifier { Color _color = Colors.red; // get Color => _color; Void randomColor() {_color = color.fromargb (255, Random().nextint (256)+0, Random().nextint (256)+0, Random().nextInt(256)+0); notifyListeners(); // Tell listeners to refresh}}Copy the code

Main function modification

void main() {
  runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider.value(value: ColorModel()),
          ChangeNotifierProvider.value(value: CounterModel())],
        child: MyApp(),
  ));
}
Copy the code

SecondProviderPage interface

class _SecondProviderPageState extends State<SecondProviderPage> { @override Widget build(BuildContext context) { return  Scaffold( appBar: AppBar(title: Text('SecondProviderPage'),), body: Column( children: Consumer2<CounterModel,ColorModel> (context,CounterModel count,ColorModel colormodel,_) { return Text('Counter: ${count.counter}', style: TextStyle(color: colormodel.color),); }), Consumer2<CounterModel,ColorModel> (context, CounterModel counter, ColorModel colormodel,child) => RaisedButton( onPressed: (){ counter.increment(); colormodel.randomColor(); }, child: Icon(Icons.add))), TestView(), ]) ); }}Copy the code

reference

  • 1, Flutter | state management guide – the Provider
  • 2. Why and how to do state management?
  • 3. Demo address