Flutter源码分析系列(二):Widget数据共享之InheritedWidget

简介

业务开发中经常会碰到这样的情况,多个Widget需要同步同一份全局数据,比如点赞数、评论数、夜间模式等等。在安卓中,一般的实现方式是观察者模式,需要开发者自行实现并维护观察者的列表。在flutter中,原生提供了用于Widget间共享数据的InheritedWidget,当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。

使用范例

先来看下官方注释中的范例:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  }) : assert(color != null),
       assert(child != null),
       super(key: key, child: child);
       
  final Color color;
  
  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }
  
  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}

实现比较简单,直接继承InheritedWidget即可,类中的color即为需要共享的数据。
此外需要实现updateShouldNotify方法,当InheritedWidget被更新时,该方法会被调用,并传入更新之前的Widget对象,该方法内需要实现新旧数据的比较,若数据不同需要通知依赖的Widget更新,则返回true。
使用时,可以通过FrogColor.of(context).color获取到它的值。
下面举一个具体的例子,简单的使用场景如下图所示:

Flutter源码分析系列(二):Widget数据共享之InheritedWidget_第1张图片
InheritedWidget范例示意图

将最上层的Widget用InheritedWidget包装起来,并设置颜色为绿色,子Widget中需要使用该颜色时调用FrogColor.of(context).color来获取,如图中的Icon和Image,此时调用该方法获取到的颜色即为绿色。当InheritedWidget的数据有变化,即通过setState将绿色改为红色后,依赖了该数据的Icon和Image会自动rebuild,构造新的Widget时,此时从FrogColor.of(context).color获取到的颜色也会变为红色,由此便实现了数据的同步。

源码分析

InheritedWidget

先来看下InheritedWidget的源码:

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  @override
  InheritedElement createElement() => new InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

它继承自ProxyWidget:

abstract class ProxyWidget extends Widget {
  const ProxyWidget({ Key key, @required this.child }) : super(key: key);

  final Widget child;
}

可以看出Widget内除了实现了createElement方法外没有其他操作了,它的实现关键一定就是InheritedElement了。

InheritedElement

class InheritedElement extends ProxyElement {
  InheritedElement(InheritedWidget widget) : super(widget);

  @override
  InheritedWidget get widget => super.widget;

  // 这个Set记录了所有依赖的Element
  final Set _dependents = new HashSet();

  // 该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1)
  @override
  void _updateInheritance() {
    final Map incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = new HashMap.from(incomingWidgets);
    else
      _inheritedWidgets = new HashMap();
    _inheritedWidgets[widget.runtimeType] = this;
  }

  // 该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新
  @override
  void notifyClients(InheritedWidget oldWidget) {
    if (!widget.updateShouldNotify(oldWidget))
      return;
    for (Element dependent in _dependents) {
      dependent.didChangeDependencies();
    }
  }
}

其中_updateInheritance方法在基类Element中的实现如下:

void _updateInheritance() {
  _inheritedWidgets = _parent?._inheritedWidgets;
}

总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:

  1. InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
  2. 如果某节点的父节点有不止一个同一类型的InheritedWidget,调用inheritFromWidgetOfExactType获取到的是离自身最近的该类型的InheritedWidget。

看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents这个列表中的呢?
回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:

@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
  // 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
  if (ancestor != null) {
    // 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作
    _dependencies ??= new HashSet();
    _dependencies.add(ancestor);
    // 这里将自己添加到了_dependents列表中,相当于注册了监听
    ancestor._dependents.add(this);
    return ancestor.widget;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

到此InheritedWidget就分析完了,相比观察者模式,使用它是不是方便了很多呢?

你可能感兴趣的:(Flutter源码分析系列(二):Widget数据共享之InheritedWidget)