In Flutter, the UI is made up of widgets of different granularity, which results in some data being passed between widgets. In general, data is passed through attributes. However, when cross-layer transmission is involved, a property may need to cross many layers to be passed to a child component, resulting in many intermediate components that do not need this property receiving data from their child widgets, which is cumbersome and redundant.

Therefore, at this time, other schemes are needed for cross-layer transmission. Currently, there are mainly three schemes involving InheritedWidget, Notification and EventBus to achieve data cross-layer transmission.

1, InheritedWidget

Inheritedwidgets are used to get data from the underlying Widget to the upper Widget, meaning that data is transferred from the parent Widget to the child Widget. In other words, the two widgets must have a parent-child relationship to use the InheritedWidget.

1.1. Use of inheritedWidgets

Here’s the use of the InheritedWidget, as follows.

Create a class that InheritedWidget
class CountContainer extends InheritedWidget {
  // Get the InheritedWidget with type CountContainer and time complexity O(1)
  static CountContainer of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountContainer>();

  final int count;

  CountContainer({Key key, @required this.count, @required Widget child})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(covariantCountContainer oldWidget) => count ! = oldWidget.count; }class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    // use CountContainer as the root node and 0 as the initialization count
    return CountContainer(count: 0, child: Counter()); }}/ / CountContainer use
class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get the InheritedWidget node
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${state.count}',),); }}Copy the code

In the code above, there is a problem. The value of count cannot be changed. However, if you want to change the count value, you can use ChangeNotifier to update the value.

class CountContainer<T extends ValueNotifier> extends InheritedNotifier {
  static CountContainer of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountContainer>();

  const CountContainer({
    Key key,
    this.notifier,
    @required Widget child,
  })  : assert(child ! =null),
        super(key: key, child: child);
  final T notifier;
}
class _MyHomePageState extends State<MyHomePage> {
  //ValueNotifier inherits from ChangeNotifier
  ValueNotifier _valueNotifier;
  int count = 0;

  @override
  void initState() {
    Create a ValueNotifier object
    _valueNotifier = new ValueNotifier(0);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CountContainer(
      notifier: _valueNotifier,
      child: GestureDetector(
        onTap: () {
          count++;
          // Update the value_valueNotifier.value = count; }, child: Counter(), ), ); }}class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${CountContainer.of(context).notifier.value}',),); }}Copy the code

With the code above, we can use InheritedWidge+ChangeNotifier to get the variable data in the parent Widget.

Data updates in providers are implemented using InheritedWidge+ChangeNotifier.

1.2 Implementation principles of InheritedWidget

Take a look at how InheritedWidget works. Its core implementation is embedded in its InheritedElement as follows.

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 as InheritedWidget;

  / / storage Element
  final Map<Element.Object> _dependents = HashMap<Element.Object> ();@override
  void _updateInheritance() {
    // Get the parent Element's _inheritedWidgets
    final Map<Type, InheritedElement> incomingWidgets = _parent? ._inheritedWidgets;if(incomingWidgets ! =null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    // Add InheritedElement to _inheritedWidgets with widget.runtimeType as key
    _inheritedWidgets[widget.runtimeType] = this;
  }
  
  @protected
  Object getDependencies(Element dependent) {
    // Retrieve the stored value from _dependents based on Element
    return _dependents[dependent];
  }

  @protected
  void setDependencies(Element dependent, Object value) {
    // Add Element to _dependents (value can be null)
    _dependents[dependent] = value;
  }

  @protected
  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    // Call Element's didChangeDependencies method, which redraws the dependent Widget
    dependent.didChangeDependencies();
  }

  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }
  @override
  void notifyClients(InheritedWidget oldWidget) {
    // Iterate according to _dependents
    for (final Element dependent in_dependents.keys) { notifyDependent(oldWidget, dependent); }}}abstract class Element extends DiagnosticableTree implements BuildContext {
  / /...
  void _updateInheritance() {
    // Assign the _inheritedWidgets value of the parent Element to the _inheritedWidgets of the current Element_inheritedWidgets = _parent? ._inheritedWidgets; }/ /...
}
Copy the code

Start with the _inheritedWidgets property, which is a Map within Element. If the current Element is an InheritedElement, add the current Element to the _inheritedWidgets, Otherwise, the _inheritedWidgets in its parent Element are copied to the _inheritedWidgets in the current Element. , that is, each Element is stored in the current Element to the root Element in all InheritedElement object, so it is also a dependOnInheritedWidgetOfExactType time complexity is O (1).

abstract class Element extends DiagnosticableTree implements BuildContext {
  / /...
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor ! =null); _dependencies ?? = HashSet<InheritedElement>(); _dependencies.add(ancestor);// Add the current Element to the '_dependents' of' InheritedElement '
    ancestor.updateDependencies(this, aspect);
    // Returns an InheritedWidget object
    return ancestor.widget;
  }
  / /...
  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    // Get an InheritedElement from _inheritedWidgets according to the type of T.
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if(ancestor ! =null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  / /...
}
Copy the code

When get InheritedWidget through dependOnInheritedWidgetOfExactType, will also add the current Element to InheritedElement _dependents.

When the notifyClients method on an InheritedElement is called, its _dependents method is iterated over and Element’s didChangeDependencies method in _dependents is executed to update the UI.

abstract class Element extends DiagnosticableTree implements BuildContext {
  / /...
  @mustCallSuper
  void didChangeDependencies() {
    // Mark the current Element as dirty. The markNeedsBuild method is also called in setState.
    markNeedsBuild();
  }
  / /...
}
Copy the code

2, Notification

Notification is mainly used to transfer data from the underlying Widget to the upper Widget, that is, the direction of data transfer is from the child Widget to the parent Widget. That is, two widgets must have a parent-child relationship to use Notification.

2.1. Use of Notification

Notification needs to be used with NotificationListener.

// Create a Notification object
class CountContainer extends Notification {
  final int count;

  CountContainer(this.count);
}

class _MyHomePageState extends State<MyHomePage> {
  int count = 0;
  int num = 0;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: NotificationListener<CountContainer>(
        // Accept the value passed by the child widget
        onNotification: (CountContainer container) {
          setState(() {
            num = container.count + 1;
          });
          // If false is returned, data can continue to be passed up
          return false;
        },
        child: Column(
          children: [
            Text("Current value of num:$num"),
            NotificationListener<CountContainer>(
              child: Column(
                children: [Text("Current count value:$count"), Counter()],
              ),
              // Accept the value passed by the child widget
              onNotification: (CountContainer container) {
                setState(() {
                  count = container.count;
                });
                // If false is returned, data can continue to be passed up
                return false; },)],),),); }}class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      child: Text("Update UI"),
      onPressed: () {
        // Pass data to the parent Widget
        CountContainer(1).dispatch(context); }); }}Copy the code

It’s easy to use, but one thing to notice here is the return value of the corresponding onNotification function. If the return value is false, the data can continue to be passed up to the root Widget. Otherwise, stop at the current Widget.

2.2 Implementation principle of Notification

The implementation of Notification is simple. It starts from the current Widget, walks up, finds the corresponding NotificationListener, and passes the data to the _dispatch method of NotificationListener for distribution. The code implementation is as follows.

abstract class Notification {
  const Notification();

  
  @protected
  @mustCallSuper
  bool visitAncestor(Element element) {
    if (element is StatelessElement) {
      final StatelessWidget widget = element.widget;
      if (widget is NotificationListener<Notification>) {
        // Find the NotificationListener and hand it to its _dispatch method to process the data
        if (widget._dispatch(this, element)) // that function checks the type dynamically
          return false; }}return true;
  }

  void dispatch(BuildContext target) {
    // Look up the specified NotificationListener from the current Widget in O(n) time
    target?.visitAncestorElements(visitAncestor);
  }
}
Copy the code

VisitAncestorElements is the implementation that walks up to the Element, while visitAncestor is the implementation that checks whether the current Widget is the specified NotificationListener.

abstract class Element extends DiagnosticableTree implements BuildContext {
  / /...
  @override
  void visitAncestorElements(bool visitor(Element element)) {
    Element ancestor = _parent;
    // If visitor returns true and ancestor is not null, the ancestor continues up
    while(ancestor ! =null && visitor(ancestor))
      ancestor = ancestor._parent;
  }
  / /...
}
Copy the code

VisitAncestorElements’ implementation is simple, as above. So the time complexity of finding a specified NotificationListener is O(n).

3, EventBus

Inheritedwidgets and Notification must require a parent-child relationship between the two widgets to transmit data. However, if the two widgets are siblings, you can’t use the InheritedWidget and Notification. At this point, You can use EventBus for data transfer, and of course widgets with parent relationships can also use EventBus for data transfer.

3.1. Use of EventBus

First you need to import EventBus as follows.

  event_bus: ^1.11.
Copy the code

With the import successful, you can now use EventBus directly.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("EventBus Demo"),
      ),
      body: Column(
        children: [
          // Accept the message Widget
          MessageWidget(),
          // Send message WidgetOtherWidget(), ], ), ); }}class Message {
  final String text;

  const Message(this.text);
}

class MessageWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MessageWidgetState();
}

EventBus eventBus = new EventBus();

// Display the message Widget
class MessageWidgetState extends State<MessageWidget> {
  String text = "Nothing.";
  StreamSubscription subscription;

  @override
  void initState() {
    subscription = eventBus.on<Message>().listen((event) {
      setState(() {
        text = event.text;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    // There is a memory leak if you do not cancelsubscription? .cancel();super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text("Current message:$text"); }}// Send message Widget
class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: () {
          eventBus.fire(Message("Message test"));
        },
        child: Text("Send message")); }}Copy the code

In the above code, the MessageWidget is sibling to the OtherWidget, so we cannot use InheritedWidget and Notification to communicate between the two widgets, but use EventBus to communicate between the two widgets.

EventBus is implemented based on Stream. For details on how to implement Flutter, see this article.

4, summarize

This article mainly introduces the communication between widgets in Flutter. Although providers are currently recommended, you still need to understand the three solutions in this article so that you can better use Provider and read the source code of Provider. Of course, this article is a precursor to analyzing how providers are implemented.