Read this article

  1. Understand the usage scenarios and rationale for the inheritedWidgets in Flutter
  2. Know the implementation of existing providers on the market
  3. Implement a simple version of MiniProvider

The main features of the Flutter Provider

  1. Data sharing
  2. Local refresh

To implement the above two function points, first take a look at the InheritedWidget in Flutter

InheritedWidget profile

Introduction of Flutter Widget

Everything is a widget. Widgets are the basic unit that “describes” the UI of Flutter.

  • Describe the hierarchy of the UI (Widget nesting)
  • Customize specific UI styles (e.g., font color, etc.)
  • Guide UI layout process (e.g. Center padding, etc.)

Widgets can be broadly divided into three categories by function

  • “Component Widgets” — composite widgets that inherit directly or indirectly from statelessWidgets or StatefulWidgets. The design of widgets follows the principle of composition over inheritance. You can combine widgets with more complex functionality than a single Widget. Normal business development is focused on developing this type of Widget;

  • “Proxy Widget” — As the name suggests, “Proxy Widget” does not involve the internal logic of the Widget itself, but provides additional intermediate functionality for the “Child Widget”. Typically, InheritedWidget is used to transfer shared information between “Descendant Widgets”, ParentDataWidget is used to configure the layout information of “Descendant Renderer Widget”;

  • “Renderer Widget” — The core Widget type that directly participates in the “Layout” and “Paint” processes. Either the “Component Widget” or the “Proxy Widget” will eventually map to the “Renderer Widget”, otherwise it will not be drawn to the screen. Of the three types of widgets, only the “Renderer Widget” has a corresponding “Render Object”.

InheritedWidget is owned by Proxies that are used to pass shared data between widgets

InheritedWidget is an important functional component of Flutter. It provides a way for data to be passed and shared from top to bottom in the widget tree. For example, we share data with the InheritedWidget in the root widget of Flutter. Then we can retrieve the shared data in any child widget! This feature comes in handy in scenarios where you need to share data in the widget tree! For example, the Flutter SDK uses inheritedWidgets to share app Theme and Locale information

InheritedWidget’s information is delivered from top to bottom

The InheritedWidget has two methods in total

  1. CreateElement () (create the corresponding Element)
  2. updateShouldNotify(covariant InheritedWidget oldWidget)
  /// Whether the framework should notify widgets that inherit from this widget.
  ///
  /// When this widget is rebuilt, sometimes we need to rebuild the widgets that
  /// inherit from this widget but sometimes we do not. For example, if the data
  /// held by this widget is the same as the data held by `oldWidget`, then we
  /// do not need to rebuild the widgets that inherited the data held by
  /// `oldWidget`.
  ///
  /// The framework distinguishes these cases by calling this function with the
  /// widget that previously occupied this location in the tree as an argument.
  /// The given widget is guaranteed to have the same [runtimeType] as this
  /// object.
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
Copy the code

Whether to notify descendants that depend on the Widget data to refresh data after the Widget is rebuilt

An _dependents Widget is used to maintain a dependent descendant within an InheritedElement

/// An [Element] that uses an [InheritedWidget] as its configuration. class InheritedElement extends ProxyElement { /// Creates an element that uses the given widget as its configuration. InheritedElement(InheritedWidget widget) : super(widget); @override InheritedWidget get widget => super.widget; final Map<Element, Object> _dependents = HashMap<Element, Object>(); @override void _updateInheritance() { assert(_active); final Map<Type, InheritedElement> incomingWidgets = _parent? ._inheritedWidgets; if (incomingWidgets ! = null) _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets); else _inheritedWidgets = HashMap<Type, InheritedElement>(); _inheritedWidgets[widget.runtimeType] = this; }Copy the code

How do descendant widgets register data dependencies? BuildContext dependOnInheritedWidgetOfExactType method is to increase in the dependency

T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect });
Copy the code

One might ask, what if an InheritedWidget uses data in an InheritedWidget but doesn’t want to update the data in future data changes?

InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
Copy the code

Here’s an example

// Define a convenient method for widgets in the subtree to get shared data // add a listen parameter, 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 the code

The State object of the StatefulWidget has a didChangeDependencies callback that is called by the Flutter Framework when the “dependency” changes. This “dependency” refers to whether the child widget uses the InheritedWidget’s data in the parent widget! [InheritedWidget] InheritedWidget [InheritedWidget] If it is not used, there is no dependency. This mechanism allows child components to update themselves when the InheritedWidget they depend on changes! For example, when the theme, locale, and so on change, the didChangeDependencies method of the widget that depends on it is called. The didChangeDependencies method calls markNeedBuild and enters the rebuild process.

The InheritedWidget feature above can be used in the partial refresh part of the Provider. The inherent feature is the ability to bind InheritedWidget dependencies to descendant components that depend on it, and automatically update the dependent descendant components when the InheritedWidget data changes! With this feature, we can save the state that needs to be shared across components in an InheritedWidget, and then reference the InheritedWidget in a child component.

MiniProvider implementation

The Flutter community’s famous Provider package is a cross-component state sharing solution based on this idea. We will discuss the usage and principles of providers in more detail.

To implement a Provider, we first need an InheritedWidget that saves shared data. Since the specific business data type is unpredictable, we use generics to define a generic InheritedProvider class that InheritedWidget:

// A generic InheritedWidget, Class InheritedProvider<T> extends InheritedWidget {InheritedProvider({@required this.data, Widget child}) : super(child: child); // The shared state uses the generic final T data; @override bool updateShouldNotify(InheritedProvider<T> old) {// If InheritedProvider<T> old returns true, we call 'didChangeDependencies' on our descendant nodes. return true; }}Copy the code

Now that we have a place to save the data, what we need to do is rebuild the InheritedProvider when the data changes. Now we have two problems:

  1. How to notify data changes?
  2. Who reconstructs the InheritedProvider?

The first problem is easy to solve. Of course we can use EventBus for event notification, but to get closer to Flutter development, we use the ChangeNotifier class provided with the Flutter SDK, which is derived from Listenable. There is also a publisheer-subscriber pattern for Flutter. The ChangeNotifier is defined as follows:

class ChangeNotifier implements Listenable { List listeners=[]; // Add (listeners); // Add (listeners); } override void removeListener(listeners) {listeners. Remove (listeners); // Notice. ForEach ((item)=>item()); }... // omit irrelevant code}Copy the code

We can add and remove listeners (subscribers) by calling addListener() and removeListener(); NotifyListeners () can trigger all listener callbacks.

Now, we can place the shared state into a Model class and make it inherit from the ChangeNotifier, so that when the shared state changes, we only need to call notifyListeners() to notify the subscribers, who then rebuild the InheritedProvider. This is also the answer to the second question! Next we implement the subscriber class:

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget { ChangeNotifierProvider({ Key key, this.data, this.child, }); final Widget child; final T data; Static T of<T>(BuildContext context) {final type = _typeOf<InheritedProvider<T>>(); final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>(); return provider.data; } @override _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>(); }Copy the code

This class inherits the StatefulWidget, and then defines a static of() method for subclasses to get the shared state (model) saved in the InheritedProvider in the Widget tree. Below we implement the corresponding _ChangeNotifierProviderState class:

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> { void update() { // If the data changes (notifyListeners are called by the Model class), rebuild the InheritedProvider setState(() => {}); } @override void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) { Also add new data to listen for if (widget.data! = oldWidget.data) { oldWidget.data.removeListener(update); widget.data.addListener(update); } super.didUpdateWidget(oldWidget); } @override void initState() {// addListener to model widget.data.addlistener (update); super.initState(); } @ override void the dispose () {/ / remove the model listener widget. The data. The removeListener (update); super.dispose(); } @override Widget build(BuildContext context) { return InheritedProvider<T>( data: widget.data, child: widget.child, ); }}Copy the code

Can see _ChangeNotifierProviderState class is the main purpose of listening to the Shared state (model) is changed to build the Widget tree. Note that _ChangeNotifierProviderState class invokes the setState () method, the widget. The child is always the same, when performing the build, so the child InheritedProvider reference is always the same child widgets, So widget.child is not rebuilt, which means the child is cached! Of course, if the Parent ChangeNotifierProvider Widget is rebuilt, its passed child may change.

Now that we have all the tool classes we need, let’s take a look at using them with an example of a shopping cart.

Shopping cart example

We need to implement a function that displays the total price of all items in the shopping cart:

  1. The total price is updated when new items are added to the cart

Define an Item class that represents Item information:

class Item { Item(this.price, this.count); double price; Int count; // Number of items //... Omit other attributes}Copy the code

Define a CartModel class that holds the cart item data:

Class CartModel extends ChangeNotifier {// Used to store the List of items in the cart Final List<Item> _items = []; UnmodifiableListView<Item> get items => UnmodifiableListView(_items); Double get totalPrice => _items.fold(0, (value, item) => value + item.count * item.price); double get totalPrice => _items.fold(0, (value, item) => value + item.count * item.price); // Add [item] to shopping cart. This is the only way to change the shopping cart from the outside. void add(Item item) { _items.add(item); // Notify the listener (subscriber), rebuild the InheritedProvider, and update the status. notifyListeners(); }}Copy the code

CartModel is the Model class to be shared across components. Finally we build the sample page:

class ProviderRoute extends StatefulWidget { @override _ProviderRouteState createState() => _ProviderRouteState(); } class _ProviderRouteState extends State<ProviderRoute> { @override Widget build(BuildContext context) { return Center(  child: ChangeNotifierProvider<CartModel>( data: CartModel(), child: Builder(builder: (context) { return Column( children: <Widget>[ Builder(builder: (context) {var cart. = ChangeNotifierProvider of < CartModel > (context); return the Text (" total price: ${cart.totalPrice}"); }), Builder(builder: Return RaisedButton(Child: Text(" Add item "), onPressed: () {/ / to add items in the shopping cart, price will be updated after adding ChangeNotifierProvider. Of < CartModel > (context). The add (Item (20.0, 1));},);}),,); }),),); }}Copy the code

Figure 7-2 shows the result of an example run.

Every time you click the “Add item” button, the total price will increase by 20. We have achieved the desired function! Some readers may wonder, does it make sense to go all the way around implementing such a simple feature? In fact, in this example, just updating a state in the same routing page, our advantage of using ChangeNotifierProvider is not obvious, but what if we make a shopping APP? Shopping cart data is typically shared across the APP, such as across routes. The advantage of ChangeNotifierProvider becomes obvious if we place it at the root of the Widget tree for the entire application, so that the entire APP can share the shopping cart data.

Although the above example is relatively simple, it clearly illustrates the principle and flow of a Provider. Figure 7-3 shows the schematic diagram of a Provider.

The ChangeNotifierProvider (subscriber) is automatically notified when the Model changes, and the InheritedWidget is rebuilt internally within ChangeNotifierProvider, and the InheritedWidget’s descendant widgets that rely on that InheritedWidget are updated.

We can see that using providers brings the following benefits:

Our business code is more data-focused, and whenever we update the Model, the UI updates automatically, rather than manually calling setState() to explicitly update the page after the state changes. The messaging of data changes is masked, so we don’t have to manually handle the publishing and subscribing of state change events; it’s all encapsulated in the Provider. This is really great and saves us a lot of work! In large and complex applications, especially when there are many states that need to be shared globally, using providers will greatly simplify our code logic, reduce the probability of error, and improve the development efficiency

To optimize the

The ChangeNotifierProvider we implemented above has two obvious drawbacks: code organization issues and performance issues, which we’ll discuss below.

Code organization issues

Let’s first look at the code that builds Text to display the total price:

Builder(builder: (context){ var cart=ChangeNotifierProvider.of<CartModel>(context); Return Text(" cart: ${totalPrice}"); })Copy the code

This code can be optimized for two things:

  1. Need to explicitly call ChangeNotifierProvider of, when APP based on CartModel many, such code would be redundant.
  2. Semantic ambiguity; Since ChangeNotifierProvider is a subscriber, the widgets that depend on CartModel are naturally subscribers, which are actually consumers of the state. If we use Builder to build, the semantics are not very clear. If we can use a Widget with unambiguous semantics, such as Consumer, the resulting code semantics will be unambiguous, and as soon as we see Consumer, we know that it depends on some cross-component or global state.

To optimize these two issues, we can encapsulate a Consumer Widget that does the following:

// This is a convenience class, Provider class Consumer<T> extends StatelessWidget {Consumer({Key Key, @required this.Builder, this.child, }) : assert(builder ! = null), super(key: key); final Widget child; final Widget Function(BuildContext context, T value) builder; @override Widget build(BuildContext context) { return builder( context, ChangeNotifierProvider.of<T>(context), // automatically get Model); }}Copy the code

Consumer implementation is very simple, it by specifying the template parameters, then the internal automatic call ChangeNotifierProvider. Of obtaining the corresponding Model, and the name of Consumer is itself has the exact semantics (Consumer). Now the above code block can be optimized to look like this:

Consumer<CartModel>(Builder: (context, cart)=> Text(" cart: ${cart. TotalPrice}");)Copy the code

Isn’t that elegant?

Performance issues

The code above also has a performance problem where the code for the add button is built:

Builder(builder: (context) { print("RaisedButton build"); Return RaisedButton(Child: Text(" Add item "), onPressed: () {ChangeNotifierProvider. Of < CartModel > (context). The add (Item (20.0, 1)); }); }Copy the code

After we click the “Add Item” button, since the total price of items in the cart will change, the Text update showing the total price is expected, but the “Add Item” button itself does not change, so it should not be rebuilt. But when we run the example, every time the “Add Item” button is clicked, the console prints a “RaisedButton Build “log, meaning that the” Add Item “button itself is rebuilt each time it is clicked! Why is that? If you already understand the InheritedWidget’s update mechanism, the answer is immediately obvious: This is because the build RaisedButton Builder to invoke the ChangeNotifierProvider. Of, that is dependent on the Widget InheritedWidget in the tree (i.e. InheritedProvider) Widget, So when the CartModel changes after the goods are added, ChangeNotifierProvider will be notified, and ChangeNotifierProvider will rebuild the subtree, so the InheritedProvider will be updated, At this point the descendant widgets that depend on it will be rebuilt.

Now that the cause of the problem is clear, how can we avoid this unnecessary refactoring? Since the button is re-built because the button has a dependency on the InheritedWidget, we can simply break or unbuild that dependency. So how do you unrely the button from the InheritedWidget? We covered this in the previous section when we introduced the InheritedWidget: Call dependOnInheritedWidgetOfExactType () and getElementForInheritedWidgetOfExactType () difference is that the former will depend on registration, while the latter will not. So we just need to ChangeNotifierProvider. Of implementation to below can be like this:

// Add a listen argument, Static T of<T>(BuildContext context, {bool listen = true}) {final type = _typeOf<InheritedProvider<T>>(); final provider = listen ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>() : context.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()?.widget as InheritedProvider<T>; return provider.data; }Copy the code

Then we change the code to:

Column(children: <Widget>[Consumer<CartModel>(Builder: (BuildContext Context, cart) =>Text(" ${cart.totalPrice}"), ), Builder(builder: (context) { print("RaisedButton build"); return RaisedButton( child: The Text (" add products "), onPressed: () {/ / listen is set to false, do not establish dependencies ChangeNotifierProvider. Of < CartModel > (context, listen: Add (Item(20.0, 1));},);})],)Copy the code

Running the above example after modification, we will see that after clicking the “Add Item” button, the console will no longer output “raise button Build”, i.e. the button will not be rebuilt. And the total price will still be updated, this is because the Consumer invokes the ChangeNotifierProvider. Listen when of value as the default value is true, so will build dependencies.

So far we have implemented a mini-provider that has the core functionality of the Provider Package on the Pub; However, our mini-version is not comprehensive. For example, it only implements a Listening ChangeNotifierProvider, but does not implement a Provider only for data sharing. In addition, our implementation does not take into account some boundaries, such as how to ensure that the Model is always a singleton when the Widget tree is rebuilt. Therefore, readers are advised to use Provider Package in actual practice. The main purpose of implementing this mini-provider in this section is to help readers understand the underlying principle of Provider Package.

Other status management packages

The Flutter community now has many packages specifically for state management. Here are a few that have high relative ratings:

The package name introduce
Provider & ScopedModel Both are based on similar principles with inheredWidget
Redux A Flutter implementation of the Redux package in the React ecosystem of Web development
Mobx Is the Flutter implementation of MobX in the React ecosystem of Web development
BLoC BLoC mode is the implementation of Flutter

conclusion

By introducing InheritedWidget, we understand the idea of sharing state, and then implement a simple Provider based on this idea. In the implementation process, we further explore the registration mechanism and update mechanism of InheritedWidget and its dependencies. By learning this article, readers should achieve two goals: first, they should have a thorough understanding of the InheritedWidget and second, the design philosophy of the Provider.