【Flutter】瞧一瞧InheritedWidget是如何实现数据共享的

218 阅读4分钟

InheritedWidget实现数据共享

我偷偷地碰了你一下,不料你像蒲公英一样散开,此后到处都是你的影子!

InheritedWidget是一个功能型组件,提供了数据在widget树中从上到下传递共享的方式,很多地方都可以看到InheritedWidget的影子,比如providerbloc等,都是依赖它来实现的。InheritedWidget子widget可以获取InheritedWidget提供的数据,数据发生变化,依赖它的widget也会随之变化。

一、先来看一下简单的使用方法:

/// 继承InheritedWidget自定义共享数据
class ShareDataWidget extends InheritedWidget {
  ShareDataWidget(
    Key? key,
    this.data,
    Widget child,
  ) : super(key: key, child: child);
 
  // 共享的数据
  final String data;

  // 子树中的widget获取数据共享
  // 这个方法会使子widget依赖了该InheritedWidget
  static ShareDataWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

  // InheritedWidget发生变化后,会回调该方法
  // 返回bool决定了是否通知子树中依赖InheritedWidget的widget
  @override
  bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
    return oldWidget.data != data;
  }
}

为了保证通过setState更改数据时,不要整个widget都重建,所以再对ShareDataWidget做一层封装HelpShareDataWidget,只改变ShareDataWidget的状态即可。

class HelpShareDataWidget extends StatefulWidget {
  HelpShareDataWidget({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  State<StatefulWidget> createState() {
    return HelpShareDataWidgetState();
  }
}

class HelpShareDataWidgetState extends State<HelpShareDataWidget> {
  int _data = 0;

  // 数据加 1,更新ShareDataWidget
  void inCreaseData() {
    setState(() {
      _data += 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ShareDataWidget(data: _data, child: widget.child);
  }
}

接下来就是使用的demo,demo使用了StatelessWidget,这样能清晰看出InheritedWidget变化后,子树依赖的widget能够同步变化。

class SeeInheritedWidgetDemo extends StatelessWidget {
  // 使用key来获取HelpShareDataWidgetState对象
  final _helpKey = GlobalKey<HelpShareDataWidgetState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: HelpShareDataWidget(
        key: _helpKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: [
            Builder(
              builder: (context) {
                print('Text build.');
                return Text('current data ${ShareDataWidget.of(context)?.data}');
              },
            ),
            Builder(
              builder: (context) {
                print('ElevatedButton build.');
                return ElevatedButton(
                  onPressed: () => _helpKey.currentState?.inCreaseData(),
                  child: Text("加1"),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

运行结果

device-2021-08-22-165848.gif

image.png

可以看到点击按钮通过inCreaseData方法更新了ShareDataWidget状态(数据)后,依赖了ShareDataWidget子widget Text也会同步更新内容。

看完这个demo会有以下疑问:

  1. demo的是StatelessWidget写的,那为什么Text能够build更新显示内容呢?
  2. context.dependOnInheritedWidgetOfExactType()是如何获取到共享的数据的呢?

下面我们一一进行解答

二、子依赖widget是怎么随着InheritedWidget变化而变化的

(1)先来看看子Widget在获取InheritedWidget的数据的时候是如何与InheritedWidget建立依赖关系的

其实子widget在获取InheritedWidget的数据的时候,就已经是依赖了InheritedWidget,所以我们在源码里面看一下context.dependOnInheritedWidgetOfExactType() 里做了什么动作。

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}
  1. 首先根据InheritedWidget的类型(指我们demo中的ShareDataWidget)在 _inheritedWidgets集合中就可以获取ShareDataWidget关联的Element对象了,_inheritedWidgets是Flutter Framework一层一层传递下来的;
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  _inheritedWidgets = _parent?._inheritedWidgets;
}
@mustCallSuper
void mount(Element? parent, Object? newSlot) {
  ....
  _updateInheritance();
}

我们可以看到在Element中它在mountactivate函数执行了调用_updateInheritance,也就是说element每次挂载和重新时,会调用该方法。那么当该方法执行的时候,element就会从上层中拿到所有的InheritedElement。而InheritedElement他最终继承了Element,并可以看到InheritedElement重写了_updateInheritance方法。

  1. 那么建立依赖的关键方法就是dependOnInheritedElement,我们继续看一下这个方法;
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  // 这里将子widget的Element关联到InheritedElement中去了,建立了依赖关系
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;
}
@protected
void updateDependencies(Element dependent, Object? aspect) {
  setDependencies(dependent, null);
}
@protected
void setDependencies(Element dependent, Object? value) {
  // _dependents是InheritedElement维护的一个集合,它收集了依赖了它的子widget的Element
  _dependents[dependent] = value;
}

注意 ancestor.updateDependencies(this, aspect) 传入的this,它就是context.dependOnInheritedWidgetOfExactType()context,这个context也就是依赖InheritedWidget子widget;所以ancestor.updateDependencies(this, aspect)方法后,子widget就与InheritedWidget完成了建立依赖关系(实际上是Element完成了建立依赖关系);所以后续使用InheritedWidget的获取数据的时候,一定要注意传入的context_dependentsInheritedElement维护的一个集合,它收集了依赖了它的子widget的Element,后续讲述如何通知子widget重新build时会用到。

(2)接下来我们再看看InheritedWidget数据发生变化的时候,是怎么通知子widget重新build

调用setState更新状态的时候,InheritedElement会执行updated方法

@override
void updated(InheritedWidget oldWidget) {
  if (widget.updateShouldNotify(oldWidget))
    super.updated(oldWidget);
}

注意updateShouldNotify方法就是ShareDataWidget重写的方法,我们在这里可以判断是否需要刷新子widget

@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}
@override
void notifyClients(InheritedWidget oldWidget) {
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  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);
  }
}

注意_dependents就是在建立依赖关系的时候收集的子widgetnotifyClients会遍历_dependents调用子ElementdidChangeDependencies,从而子widget重新build刷新界面

@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  dependent.didChangeDependencies();
}