Flutter state management - in-depth understanding of InheritedWidget

what is state, state management

In reactive programming, the state is the data, and when the state changes, the page changes. There are two types of state changes:

  • Changes to the current page state
  • State data sharing across widgets

And state management, in more cases, is aimed at state management across widgets.

Commonly used state management:

  • InheritedWidget
  • scoped_model
  • Provider
  • flutter_redux

Overview

InheritedWidget is a functional widget without an interface. Its main function is to share data from top to bottom in the widget tree.

A small example, first know how to use it.

Still take the counter as an example, the outline process:

  • Customize a class MyInheritedWidget that inherits InheritedWidget. Define the data to be shared; override the method updateShouldNotify, which is used to determine under what conditions to notify the update; provide the of method to obtain an instance of MyInheritedwidget.
  • Customize DependWidget, which uses the field data of MyInheritedWidget internally.
  • When building the widget tree, make MyInheritedDepend the parent class of DependWidget.
  • When the data of MyInheritedWidget changes, the updateShouldNotify method will be called to determine whether it is updated. If it is updated, the dirty of DependWidget will be finally set to true and refreshed in the next frame. At the same time, the didChangeDependencies method of DependWidget is called back.
  • Special emphasis: the dependant must become the descendant widget of MyInheritedWidget, otherwise the data cannot be shared.

Customize MyInheritedWidget to define shared data

class MyInheritedWidget extends InheritedWidget {
  final int data;
  MyInheritedWidget({
    @required this.data, //待共享的数据
    Widget child,
  }) : super(child: child);

  //获取当前MyInheritedWidget的实例
  static MyInheritedWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
  }

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    // 新旧数据不一致时,返回true,通知依赖本widget的子widget,此时子widget中的didChangeDependencies方法会被调用
    return oldWidget.data != data;
  }
}
复制代码

Depends on the data in MyInheritedWidget

class DependWidget extends StatefulWidget {
  @override
  DependWidgetState createState() {
    return new DependWidgetState();
  }
}

class DependWidgetState extends State<DependWidget> {
  @override
  Widget build(BuildContext context) {
    print('build..............');
    // 使用依赖的InheritedWidget中的数据
    return Text(MyInheritedWidget.of(context).data.toString());
  }

  // 当依赖的InheritedWidget中的数据发生变化时,调用此方法
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('didChangeDependencies.....');
  }

  @override
  void didUpdateWidget(DependWidget oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }
}
复制代码

When shared data changes, dependencies change.

import 'package:flutter/material.dart';

class InheritedWidgetTest extends StatefulWidget {
  @override
  _InheritedWidgetTestState createState() => _InheritedWidgetTestState();
}

class _InheritedWidgetTestState extends State<InheritedWidgetTest> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedWidgetTest"),
      ),
      body: Center(
        child: MyInheritedWidget(
          data: count, // setState后  共享数据变化
          child: Center(
            child: DependWidget(), // 共享数据变化,影响该widget
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(
            () {
              ++count; // 数据变化
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
复制代码

Go deep into the source code to understand

1. of in MyInheritedWidget

of has two functions:

  • Get an instance of MyInheritedWidget.
  • Add DepentWidget to MyInheritedWidget's dependency list.

Define of in MyInheritedWidget:

static MyInheritedWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}

The return value of this method is MyInheritedWidget, obviously its purpose is to obtain an instance of MyInheritedWidget, and the BuildContext parameter needs to be passed in. The call is in DependWidget:

@override
Widget build(BuildContext context) {
  // 使用依赖的InheritedWidget中的数据
  return Text(MyInheritedWidget.of(context).data.toString());
}

Note that the data here is the data to be shared, and the context passed in is the context of the build method in DependWidgetState.

So how does the of method get MyInheritedWidget?

Trace context.dependOnInheritedWidgetOfExactType() to Element:

@override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

There are two key points:

  • final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; Obtain the instance of InheritedElement according to the generic type, note that it is not InheritedWidget.
  • return dependOnInheritedElement(ancestor, aspect: aspect) as T; Do some operations on InheritedElement, and then return MyInheritedWidget inside the method.

First look at _inheritedWidget, which is an attribute defined in Element:

Map<Type, InheritedElement> _inheritedWidgets;

The saved content is: in the current widget tree, the corresponding relationship between all the generic classes of the InheritedWidget system and the InheritedElement. This is why the InheritedElement ancestor can be obtained through _inheritedWidgets[T], where T is MyInheritedWidget, and the ancestor here is actually the InheritedElement corresponding to MyInheritedWidget.

So when did InheritedElement enter _inheritedWidgets? Continue to view the source code, in the mount method of element:

void mount(Element parent, dynamic newSlot) {
  。。。。
  _updateInheritance();
  。。。。
}
void _updateInheritance() {
  assert(_active);
  _inheritedWidgets = _parent?._inheritedWidgets;
}

That is to say, when the Element is used, the _inheritedWidgets will be updated through the _updateInheritance method, and the _inheritedWidgets of the current Element will be inherited from the parent. But it should be noted that the above _updateInheritance is the default implementation, and InheritedElement overrides this method:

@override
  void _updateInheritance() {
    ......
    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;
  }

First, get _inheritedWidgets from the parent first, if it is null, create a new one; if it is not null, copy a copy of the data. Finally _inheritedWidgets[widget.runtimeType] = this; put itself in.

In general, each Element has an _inheritedWidgets field. If it is a non-InheritedElement, then _inheritedWidgets is obtained from the parent. If it is an InheritedElement, then _inheritedWidgets is copied from the parent first, and then adds itself into it. In this way, in the entire Element tree, _inheritedWidgets are passed down layer by layer. This is why the previous InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; can get the InheritedElement of MyInheritedWidget (when the InheritedElement of MyInheritedWidget is mounted, it has already added itself).

Look at dependOnInheritedElement again:

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

The InheritedElement ancestor here is the InheritedElement of MyInheritedWidget, so the last return ancestor.widget returns is MyInheritedWidget.

Here ancestor.updateDependencies(this, aspect), this is DependWidget (because of is called in DepentWidget, and context is the context of DepentWidget's build method), continue to go deeper:

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

@protected
void setDependencies(Element dependent, Object value) {
  _dependents[dependent] = value;
}

final Map<Element, Object> _dependents = HashMap<Element, Object>();

_dependents is an attribute under InheritedElement, which saves: all Widgets that use the shared data of MyInheritedWidget. That is to say, here, put DependWidget into _dependents, that is, when a widget obtains InheritedWidget through of, it will add itself to the dependency list of the InheritedWidget.

Looking back at it again:

  _dependencies ??= HashSet<InheritedElement>();
  _dependencies.add(ancestor);

_dependencies is an attribute defined under Element (that is, every xxxElement that inherits Element has this attribute):

Set<InheritedElement> _dependencies;
复制代码

It can be seen from the definition that it saves all InheritedElements. When the of method is called, the InheritedElement of the specified generic type is added to the set. In the subsequent notifyClient, it is necessary to judge whether DepentWidget really depends on MyInheritedWidget.

@override
  void notifyClients(InheritedWidget oldWidget) {
    assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
    for (final Element dependent in _dependents.keys) {
      。。。。。。
      // check that it really depends on us
      assert(dependent._dependencies.contains(this));
      notifyDependent(oldWidget, dependent);
    }
  }
}

So far the of method is all done.

2. Use of updateShouldNotify

When the shared data changes, it is judged whether to notify DependWidget to update according to the return value of this method.

@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
  return oldWidget.data != data;
}

In this example, the origin of the change:

floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(
            () {
              ++count; // 数据变化
            },
          );
        },
        child: Icon(Icons.add),
      ),

What happens after setState?

The reason why we can call the setState method directly is because the method is defined in State:

@protected
  void setState(VoidCallback fn) {
    。。。。。
    _element.markNeedsBuild();
  }

Continue, _element.markNeedsBuild(), obviously this method is in Element:

/// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame.
void markNeedsBuild() {
  。。。。。。
  if (dirty)
    return;
  _dirty = true;
  owner.scheduleBuildFor(this);
}

Mark the element's _dirty to true and add it to the global widgets collection to have it rebuild on the next frame. The updated method is called before build. Note that this is called in _InheritedWidgetTestState. When building the widget tree, when the InheritedWidget is reached, the updated method of the InheritedElement will be triggered before the build:

/// Calls [Element.didChangeDependencies] of all dependent elements, if
  /// [InheritedWidget.updateShouldNotify] returns true.
  ///
  /// Called by [update], immediately prior to [build].
  ///
  /// Calls [notifyClients] to actually trigger the notifications.
  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }

Here we will judge the return value of the updateShouldNotify method, true continues to call super, continue to look at super, it is ProxyElement:

/// Called during build when the [widget] has changed.
///
/// By default, calls [notifyClients]. Subclasses may override this method to
/// avoid calling [notifyClients] unnecessarily (e.g. if the old and new
/// widgets are equivalent).
@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

/// Notify other objects that the widget associated with this element has
/// changed.
///
/// Called during [update] (via [updated]) after changing the widget
/// associated with this element but before rebuilding this element.
@protected
void notifyClients(covariant ProxyWidget oldWidget);

The notifyClients here is an empty implementation, so let's move on to InheritedElement:

/// Notifies all dependent elements that this inherited widget has changed, by
  /// calling [Element.didChangeDependencies].
  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      assert(() {
        // check that it really is our descendant
        Element ancestor = dependent._parent;
        while (ancestor != this && ancestor != null)
          ancestor = ancestor._parent;
        return ancestor == this;
      }());
      // check that it really depends on us
      assert(dependent._dependencies.contains(this));
      notifyDependent(oldWidget, dependent);
    }
  }
}
@protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }

Note that the above mentioned: dependent._dependencies.contains(this) is used here; the dependent here is the DependWidget we used MyInheritedWidget earlier, which calls back to the user's life cycle didChangeDependencies().

However, the build method of DependWidget is still not triggered, how to trigger it? Pay attention to the position of setState above. DependWidget is a Statefulwidget. At this time, the didUpdateWidget life cycle method of DependWidget will be triggered, and then the build method will be triggered, thereby updating the data.

Summary of key processes

1. What is done during the construction of the tree

  • During the creation of each Widget, the corresponding Element is actually called.
  • The mount method is called when an Element is added to the render tree.
  • The _updateInheritance method inside the mount method.
  • Note that Widget is a tree, Element layers wear parts, and the _updateInheritance method is called for each Element. If it is currently a non-InheritedWidget, it inherits the parent's _inheritedWidgets. If it is an InheritedWidget, it inherits the parent's _inheritedWidgets and adds itself.
  • MyInheritedWidget puts itself in _inheritedWidgets.
  • When the Element of DependWidget is mounted, it holds _inheritedWidgets, including MyInheritedWidget (actually InheritedElement)

2. How to get shared data

  • Use MyInheritedWidget.of(context).data.toString() in the build method of DependWidget to get the shared state data
  • The of method of MyInheritedWidget actually calls context.dependOnInheritedWidgetOfExactType()
  • The context is actually Element, and then call the dependOnInheritedWidgetOfExactType() method of Element, call InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T], get the InheritedElenent of MyInheritedWidget, and then continue to call dependOnInheritedElement() internally, first of all _dependencies update, Add InheritedElement to Set, then ancestor.updateDependencies() - setDependencies() - _dependents[dependent] = value, so DependWidget is added to InheritedElement's dependency list. Finally, the return ancestor.widget;of method in dependOnInheritedElement is called internally to get the example of MyInheritedWidget.

3. Data refresh process

  • _InheritedWidgetTestState calls setState
  • It is actually the setState method of State, which internally calls the markNeedsBuild() method of Element.
  • markNeedsBuild() internally marks the _dirty of Element as true, indicating that it will rebuild in the next frame.
  • _InheritedWidgetTestState 重新 build。
  • During the rebuilding of the widget tree, before the MyInheritedWidget is built, the system calls the updated method, internally widget.updateShouldNotify(oldWidget), to determine whether to notify DependWidget to update. If it is true, the notifyClients method is called, and the notifyClients internally traverses all the dependants (DependWidget), and calls the didChangeDependencies method of the dependants one by one.
  • In the process of rebuilding the widget tree, since DependWidget is a StatefulWidget, the state is no longer created, but the didUpdateWidget method is called, and the build method of the DependWidget is called, and the of method is called in the build to obtain the data of the relationship and refresh the data.

4. The connection between state and element's didChangeDependencies

In fact, our element and state's didChangeDependencies methods are two completely different methods, but after the element's didChangeDependencies method triggers an update, the state's didChangeDependencies callback is often triggered. As we all know, the state's didChangeDependencies is generally called after firstBuild, which is initState. , In fact, there is another calling time, that is, after the InheritedWidget is updated, as long as updateShouldNotify is true, the dependent class will be triggered. How is it triggered? We can look at the source code. When we analyzed the triggering of markNeedsBuild() above, The triggering of the rendering pipeline will inevitably trigger the performRebuild callback of the element. The implementation of performRebuild in StatefulElement is

  @override
  void performRebuild() {
    if (_didChangeDependencies) {
      _state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }

After the performRebuild callback, it will judge whether the value of _didChangeDependencies is true. If it is, it will call back _state.didChangeDependencies(). When is _didChangeDependencies true?

 @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
  }

In fact, when the element's didChangeDependencies method is triggered before, _didChangeDependencies is already true, so as long as the element's didChangeDependencies is triggered, it usually follows the state's didChangeDependencies method.

5. The difference between context.dependOnInheritedWidgetOfExactType() and context.findAncestorWidgetOfExactType

context.findAncestorWidgetOfExactType can also find the parent node, so how does he implement it?

  @override
  T findAncestorWidgetOfExactType<T extends Widget>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while (ancestor != null && ancestor.widget.runtimeType != T)
      ancestor = ancestor._parent;
    return ancestor?.widget as T;
  }

It can be seen that this method is relatively rough, starting from the current node, recursing to the parent node, and returning directly when the runtimeType is found to be the same as the generic type, so compared with context.dependOnInheritedWidgetOfExactType(), the efficiency of this method will be particularly low, but This method is not useless, because we just knew that context.dependOnInheritedWidgetOfExactType() can only be found when the parent node is InheritedWidget, but the findAncestorWidgetOfExactType method throws away this limitation and can search for any one directly and violently. node.

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/123304250