Flutter | data sharing

Posted by JPnyc on Sat, 12 Feb 2022 01:52:43 +0100

Sample code for this article

Data sharing InheritedWidget

InheritedWidget is a very important functional component in fluent. It provides a way to transfer data from top to bottom in the widget tree. For example, if a data is shared in the root widget through InheritedWidget, we can obtain the shared data in any child widget;

This feature is very convenient in some scenes that need to share data in the widget tree. For example, the Fluter SDK uses this widget to share application theme and locale information;

didChangeDependencies

The callback is the of the State object. It will be called by the fluent framework when the dependency changes. This dependency refers to whether the widget uses the data of the InheritedWidget in the parent widget;

If used, it means that the component depends on InheritedWidget. If not used, it means that there is no dependency.

This mechanism enables the InheritedWidget that the child component depends on to update itself when it changes. For example, when the theme changes, the didChangeDependencies method of the dependent child widget will be called

Here's a chestnut:

class ShareDataWidget extends InheritedWidget {
  //Data to be shared
  final int data;

  ShareDataWidget({@required this.data, Widget child}) : super(child: child);

  //Define a convenient method to facilitate widget s in the subtree to obtain shared data
  static ShareDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

  ///This callback determines whether to notify the widget s that depend on data in the subtree when the data changes
  @override
  bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
    //Return true: the didChangeDependencies of widgets that depend on the current widget in the subtree will be called
    return oldWidget.data != data;
  }
}
Copy code

The above defines a shared ShareDataWidget, which inherits from inheritedwidwidget and saves a data attribute, which is the data to be shared

class TestShareWidget extends StatefulWidget {
  @override
  _TestShareWidgetState createState() => _TestShareWidgetState();
}

class _TestShareWidgetState extends State<TestShareWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(ShareDataWidget.of(context).data.toString());
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('Change');
  }
}
Copy code

The above implements a sub component, uses the data of ShareDataWidget in the build method, and prints the log in the callback

Finally, create a button and click once to increase the value of ShareDataWidget

class TestInheritedWidget extends StatefulWidget {
  @override
  _TestInheritedWidgetState createState() => _TestInheritedWidgetState();
}

class _TestInheritedWidgetState extends State<TestInheritedWidget> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ShareDataWidget(
        data: count,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: TestShareWidget(),
            ),
            RaisedButton(
              child: Text("Increment"),
              //With each click, the count will increase automatically, and then re build, and the ShareDataWidget will be updated
              onPressed: () => setState(() => ++count),
            )
          ],
        ),
      ),
    );
  }
}
Copy code

The effect is as follows:

It can be seen that after the dependency changes, the did of the sub component Method will be called. It should be noted that if the build method of TestShareWidget does not use the data of ShareDataWidget, its did Method will not be called because it does not rely on ShareDataWidget;

For example, change to the following:

class _TestShareWidgetState extends State<TestShareWidget> {
  @override
  Widget build(BuildContext context) {
    // return Text(ShareDataWidget.of(context).data.toString());
    return Text("test");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('Change');
  }
}
Copy code

After commenting out the code that relies on ShareDataWidget in the buid method, a fixed Text is returned. In this way, although the data has changed, there is no dependency, so the didChangeDependencies method will not be called

Because when the data changes, it is reasonable and performance friendly to update only the Widget of the data

It should be in did What to do in the method

Generally speaking, child widget s rarely re this method. It should be that the build method will also be called after the dependency changes. However, if you need to do some expensive operations when the dependency changes, such as network requests, the best way is to execute them in this method, which can avoid these expensive operations every time you build

In depth understanding of InheritedWidget method

If we only want to rely on data and do not want to execute the didChangeDependencies method when relying on changes, what should we do as follows:

//Define a convenient method to facilitate widget s in the subtree to obtain shared data
static ShareDataWidget of(BuildContext context) {
  // return context.dependOnInheritedWidgetOfExactType();
    return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
Copy code

Change the way to get ShareDataWidget to context getElementForInheritedWidgetOfExactType(). Just the widget

So what is the difference between the two methods? Let's take a look at the source code:

@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  return ancestor;
}
Copy code
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
   //Compared with the above code, there are more parts
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}
Copy code

It can be seen that the dependOnInheritedWidgetOfExactType has more dependOnInheritedElement methods than getElementForInheritedWidgetOfExactType. Its source code is as follows:

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;
}
Copy code

It can be seen that dependencies are mainly registered in the dependOnInheritedElement method. Therefore, when getelementforinheritedwiddgetofexacttype is called, the InheritedWidget and its dependent descendant components are registered. After that, when the InheritedWidget changes, the dependent descendant components can be updated, That is, the build and didChangeDependencies methods of descendant components.

Because getElementForInheritedWidgetOfExactType has no registration dependency, the widget of the corresponding descendant will not be updated when the InheritedElement changes

Note: in the above example, after changing the of method to getElementForInheritedWidgetOfExactType_ The didChangeDependencies method of TestShareWidgetState will not be called, but the build method is called; This should be called after clicking the button_ The setState method of TestInheritedWidgetState. At this time, the page will be rebuilt, which will cause TestShareWidget() to be rebuilt, so its build will also be executed

In this case, components that rely on ShareDataWidget only need to call_ The setState method of TestInheritedWidgetState will be re built, which is unnecessary. What can be avoided? The answer is to use cache;

A simple way is to cache the Widget tree by encapsulating a stateful Widget, so that it can be executed in build;

Cross component state sharing Provider

In fluent, the general principles of state management are:

  • If the component is private, the component manages the state itself
  • If shared across components, the state is managed by a common parent component

There are many ways to manage cross component shared state, such as using the global practice bus EventBus, which is the implementation of observer mode. Through it, cross component state synchronization can be realized: state holder: update state, publish state and use; The state user (observer) monitors the state change event to complete some operations;

However, there are some obvious disadvantages of implementing cross components through observer mode:

  • It is inconvenient to explicitly define various events
  • Subscribers must explicitly register state change callbacks and manually unbind callbacks when components are destroyed to avoid memory leaks

Is there a better management method? The answer is yes. It uses InheritedWidget. Its natural feature is that it can bind the dependency between InheritedWidget and its descendant components, and can automatically rely on descendant components when data changes!, Using this feature, we can save the state that needs to cross components in the InheritedWidget, and then reference the InheritedWidget in the sub component. The famous Provider package in the Flutter community is a set of cross component state sharing solutions based on this idea. Let's take a detailed look at the usage and principle of Provider.

Provider

We implement a minimum function Provider step by step according to the implementation idea of InheritedWidget learned above

Define an InheritedWidget that needs to save data

///A general InheritedProvider that stores any state that needs to be shared across components
class InheritedProvider<T> extends InheritedWidget {
  ///Shared state uses generics
  final T data;

  InheritedProvider({@required this.data, Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    ///Return true, which means that the didChangeDependencies of the descendant node will be called every time
    return true;
  }
}
Copy code

Because the specific business data types are unpredictable, generics are used here for generality

Now there is a place to save the data. The next thing to do is to rebuild the InheritedProvider when the data changes. Now we face two problems:

  1. How to notify when data changes?
  2. Who will rebuild InheritedProvider?

The first problem is actually easy to solve. We can use EventBus for notification. However, in order to be closer to the development of Flutter, we use the ChangeNotifier class provided in the Flutter SDK, which inherits from Listenable and implements a Flutter style subscriber mode. The definition is roughly as follows:

class ChangeNotifier implements Listenable {
  List listeners=[];
  @override
  void addListener(VoidCallback listener) {
     //Add listener
     listeners.add(listener);
  }
  @override
  void removeListener(VoidCallback listener) {
    //Remove listener
    listeners.remove(listener);
  }
  
  void notifyListeners() {
    //Notify all listeners and trigger the listener callback 
    listeners.forEach((item)=>item());
  }
   
  ... //Omit irrelevant code
}
Copy code

We can use add and remove to add and remove listeners. Through notifyListeners, we can trigger the callback of all listeners

Next, we put the state to be shared into a Model class and inherit it from ChangeNotifier. In this way, when the state of the shared state changes, we only need to call notifyListeners to notify the subscriber, and then the subscriber will rebuild the InheritedProvider. This is also the answer to the second question. The implementation of the subscription class is as follows:

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  final Widget child;
  final T data;

  ChangeNotifierProvider({Key key, this.child, this.data});

  @override
  _ChangeNotifierProviderState<T> createState() =>
      _ChangeNotifierProviderState<T>();

  ///Define a convenient method to facilitate widget s in the subtree to obtain shared data
  static T of<T>(BuildContext context) {
    final provider =
        context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
    return provider.data;
  }
}

class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {

  void update() {
    setState(() {});
  }

  @override
  void initState() {
    //Add listener to model
    widget.data.addListener(update);
    super.initState();
  }

  @override
  void dispose() {
    //Remove listener for model
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant ChangeNotifierProvider<T> oldWidget) {
    //When the Provider is updated, if the old data is not = =, unbind the monitoring of the old data and add the monitoring of the new data at the same time
    if (widget.data != oldWidget.data) {
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}
Copy code

ChangeNotifierProvider inherits from StatefulWidget and defines a of static method for subclasses to easily obtain the shared state saved in InheritedProvider of Widget tree

_ The main function of the ChangeNotifierProviderState class is to listen and rebuild the Widget tree when the sharing state changes. Notice that the setState() method is invoked in this class, Widget. The child is always the same, and the child of the inheritedprovider always refers to the same child Widget, so the Widget The child will not rebuild, which is equivalent to caching the child. Of course, if the ChangeNotifierProvider abdominal Widget is rebuilt, the incoming child may change

Now that all the tool classes we need have been completed, let's see how to use the above classes according to an example

Shopping cart example

///Item class, which is used to represent the information of goods
class Item {
  final price;
  int count;

  Item(this.price, this.count);
}

class CarMode extends ChangeNotifier {
  //The user saves the list of items in the shopping cart
  final List<Item> _items = [];

  //It is forbidden to modify the product information in the shopping cart
  UnmodifiableListView get items => UnmodifiableListView(_items);

  //Total price of goods in shopping cart
  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  void add(Item item) {
    _items.add(item);
    //Notify the listener (observer), rebuild the InheritedProvider, and update the status
    notifyListeners();
  }
}

class ProviderTest extends StatefulWidget {
  @override
  _ProviderTestState createState() => _ProviderTestState();
}

class _ProviderTestState extends State<ProviderTest> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ChangeNotifierProvider(
        data: CarMode(),
        child: Builder(
          builder: (context) {
            return Column(
              children: [
                Builder(builder: (context) {
                  var cart = ChangeNotifierProvider.of<CarMode>(context);
                  return Text("Total price: ${cart.totalPrice}");
                }),
                Builder(
                  builder: (context) {
                    return RaisedButton(
                      child: Text("Add item"),
                      onPressed: () {
                        ChangeNotifierProvider.of<CarMode>(context)
                            .add(Item(20, 1));
                      },
                    );
                  },
                )
              ],
            );
          },
        ),
      ),
    );
  }
}
Copy code

Item class: used to save the information system of goods

CartMode class: the class that saves the above data in the shopping cart, that is, the model class that needs to be shared across components

ProviderTest: final built page

Each time you click to add a product, the total price will increase by 20. Although this example is relatively simple, and only one status in the same routing page is updated, if it is a real shopping cart, its shopping cart data will usually be shared in the app, such as cross routing. Put ChangeNotifierProvider on the root of the Widget tree of the whole application, Then the whole app can share the data of the shopping cart

The schematic diagram of the Provider is as follows:

After the Model changes, it will automatically notify the ChangeNotifierProvider (subscriber). The InheritedWidget will be rebuilt inside the ChangeNotifierProvider, and the descendant widgets that depend on the InheritedWidget will be updated

We can find that using Provider will bring the following benefits:

1. Our business code pays more attention to data. If we only need to update the Model, the UI will be updated automatically, instead of manually calling setState to explicitly update the page after the state changes

2. The message transmission of data change is blocked. We don't need to manually handle the publishing and subscription of change events. All these are encapsulated in the Provider, which saves us a lot of work

3. In large and complex applications, especially when there are many states that need to be shared globally, using Provider will greatly simplify our code logic, reduce error probability and improve development efficiency

optimization

The ChangeNotifierProvider implemented above has two obvious disadvantages: code organization and performance. Let's take a look

Code organization problem
Builder(builder: (context) {
  var cart = ChangeNotifierProvider.of<CarMode>(context);
  return Text("Total price: ${cart.totalPrice}");
}),
Copy code

There are two points to optimize this code

1. Call the ChangenotifierProvider to be displayed. When the APP relies heavily on CartMode internally, such code will be redundant

2. The semantics is not clear. Because the ChangenotifierProvider is a subscriber, the Widget relying on CarMode is naturally a subscriber, which is actually a Consumer of status; If we use Builder to build, the semantics is not very clear. If we can use a Widget with more clear semantics, such as Consumer, the language of the final code will be very clear. As long as we see the Consumer, we will know that it is a cross component or global state.

To optimize this problem, we can encapsulate a Consumer Widget as follows:

class Consumer<T> extends StatelessWidget {
  
  final Widget child;
  final Widget Function(BuildContext context, T value) builder;

  Consumer({Key key, @required this.builder, this.child});

  @override
  Widget build(BuildContext context) {
    return builder(context, ChangeNotifierProvider.of<T>(context)); //Get model automatically
  }
  
}
Copy code

Cusumer implementation is very simple. It specifies template parameters, and then automatically calls changenotifierprovider internally Of gets the corresponding Mode, and the name Consumer itself has exact semantics (Consumer). Now the above code can be optimized as follows:

Consumer<CarMode>(
  builder: (context, cart) => Text("Total price: ${cart.totalPrice}"),
),
Copy code

Isn't it elegant?

Performance issues

There is another performance problem in the above code. Where the button is added:

Builder(
  builder: (context) {
    return RaisedButton(
      child: Text("Add item"),
      onPressed: () {
        ChangeNotifierProvider.of<CarMode>(context)
            .add(Item(20, 1));
      },
    );
  },
)
Copy code

After clicking the add item button, the total price of the shopping cart will change, so the Text displayed in the summary is expected.

However, the add item button itself has not changed, so it should not be rebuilt, but the operation found that the button will be rebuilt every time it is clicked. What is this? It's because RadisedButton's build calls ChangeNotifierProvider.. Of (), that is, it depends on the InheritedWidget (i.e. InheritedProvider) Widget on the Widget tree,

Therefore, when the goods are added, the CartMode changes, and the subtree will be notified that the InheritedProvider will be updated. At this time, the dependent descendants Wdiget will be rebuilt.

The problem is found, so how to avoid this unnecessary refactoring?

Since the problem is that the button and InheritedWidget establish a dependency relationship, we just need to break this dependency relationship. How to break it? We talked about it at the top: the difference between calling dpendinheritedwidgetofexacttype and getElementForInheritedWidgetOfExactType is that the former will register the dependency relationship, while the latter will not, All you need to do is change notifierprovider The implementation of of can be changed as follows:

///listen: is the relationship established
static T of<T>(BuildContext context, {bool listen = true}) {
  final provider = listen
      ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
      : context
          .getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()
          ?.widget as InheritedProvider<T>;
  return provider.data;
}
Copy code

Run it again after modification, and you will find that the button will not be rebuilt, but will be updated.

So far, we have implemented a mini version of the Provider, which has the core functions of the Provider Package on the Pub. However, because our functions are not comprehensive, we have only implemented a listenable changenotifierprovider without data sharing. In addition, some boundary values are not taken into account in our implementation, For example, how to ensure that the single tree is always rebuilt in the Mode. Therefore, it is recommended to use the Provider Package in the project. This article is just to help you understand the underlying principle of the Provider Package

This article refers to Fluuter's practical books