51. Widgets of fluent are shared across components (Provider)

In the development of fluent, state management is an eternal topic. The general principle is: if the state is private to the component, it should be managed by the component itself; If the state is to be shared across components, it should be managed by the common parent element of each component. It is easy to understand the state management of component private, but there are many ways to manage the state shared across components. For example, using the global event bus EventBus, it is an implementation of observer mode, through which cross component state synchronization can be realized: the state holder (publisher) is responsible for updating and publishing the state, The state user (observer) listens for state change events to perform some operations. Let's look at a simple example of login status synchronization:

Define events:

enum Event{
  login,
  ... //Omit other events
}

The login page code is roughly as follows:

// Publish status change event after login status change
bus.emit(Event.login);

Page dependent on login status:

void onLoginChanged(e){
  //Login status change processing logic
}

@override
void initState() {
  //Subscribe to login status change events
  bus.on(Event.login,onLogin);
  super.initState();
}

@override
void dispose() {
  //Unsubscribe
  bus.off(Event.login,onLogin);
  super.dispose();
}

We can find that there are some obvious disadvantages of cross component state sharing through observer mode:

  1. Various events must be explicitly defined, which is difficult to manage.
  2. The subscriber must explicitly register the state change callback, and must manually unbind the callback when the component is destroyed to avoid memory leakage.

Is there a better way of cross component state management in fluent? The answer is yes, so how? Let's think about the InheritedWidget introduced earlier. Its natural feature is that it can bind the dependency between the InheritedWidget and its dependent descendant components, and when the InheritedWidget data changes, it can automatically update the dependent descendant components! Using this feature, we can save the state that needs to be shared across components in the InheritedWidget, and then reference the InheritedWidget in the sub components. The famous Provider package in the fluent community is a set of cross component state sharing solutions based on this idea. Next, we will introduce the usage and principle of the Provider in detail.

Provider

In order to enhance readers' understanding, we don't directly look at the source code of the Provider package. On the contrary, I will take you to implement a minimum function Provider step by step according to the idea of implementation through InheritedWidget described above.

First, we need an inheritedwidwidget to save the data to be shared. Because the specific business data type is unpredictable, for universality, we use generics to define a general InheritedProvider class, which inherits from the inheritedwidwidget:

// A generic InheritedWidget that saves the state that needs to be shared across components
class InheritedProvider<T> extends InheritedWidget {
  InheritedProvider({
    required this.data,
    required Widget child,
  }) : super(child: child);

  final T data;

  @override
  bool updateShouldNotify(InheritedProvider<T> old) {
    //Simply return true here, and each update will call the 'didChangeDependencies' of the dependent descendant node.
    return true;
  }
}

There is a place to save the data. What we need to do next is to rebuild the InheritedProvider when the data changes. Now we face two problems:

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

The first problem is actually easy to solve. Of course, we can use the eventBus described earlier for event notification, but 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 also implements a publisher subscriber mode of Flutter style. The definition of ChangeNotifier 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
}

We can add and remove listeners (subscribers) by calling addListener() and removeListener(); All listener callbacks can be triggered by calling notifyListeners().

Now, we put the state to be shared into a Model class, and then let it inherit from ChangeNotifier. In this way, when 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! Next, we will implement the subscriber class:

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  ChangeNotifierProvider({
    Key? key,
    this.data,
    this.child,
  });

  final Widget child;
  final T data;

  //Define a convenient method to facilitate widget s in the subtree to obtain shared 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>();
}

This class inherits StatefulWidget, and then defines a static method of() for subclasses to easily obtain the shared state (model) saved in InheritedProvider in Widget tree. Next, we implement the corresponding method of this class_ ChangeNotifierProviderState class:

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
  void update() {
    //If the data changes (notifyListeners is called by the model class), rebuild the InheritedProvider
    setState(() => {});
  }

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

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

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

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

Can see_ The main function of the changenotifierproviderstate class is to rebuild the widget tree when listening for changes in the shared state (model). Attention, in_ The setState() method is called in the ChangeNotifierProviderState class, widget. The child is always the same, so when building, the child of 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 parent widget of ChangeNotifierProvider is rebuilt, the child passed in may change.  

Keywords: iOS Android Android Studio Flutter

Added by premiso on Fri, 28 Jan 2022 09:46:35 +0200