携手创造,一起成长!这是我参加「日新方案 8 月更文挑战」的第2天,点击检查活动概况

Flutter的众多Widget傍边,有效果于烘托的RenderObjectWidget、聚集于功用整合的StatefulWidget。可是,还有一个大类,ProxyWidget也相同值得咱们重视。

[Flutter] ProxyWidget和Element更新的正确姿势

与其相关的有两个大类:

  • InhertedWidget
  • ParentDataWidget(代表:Positioned、Expanded)

这两个Widget,无非都是数据的向下传递,其一InheritedWidget更多的是事务数据,比如用户的ID、购物车的条目等等,而ParentDataWidget一般都是视图的数据,Stack需求运用parentData参数中的长宽偏移量来完结对子Widget的定位

所以,咱们能够依据ProxyWidget的子类,向上预先给ProxyWidget扣一个数据同享的「帽子」。

1. ProxyWidget和ProxyElement的主要功用

ProxyWidget自身是抽象的,需求咱们重写它的createElement()办法:

class CustomProxyWidget extends ProxyWidget {
  const CustomProxyWidget({required Widget child}) : super(child: child);
  @override
  Element createElement() => CustomProxyElement(this);
}

而ProxyElement则要重写notifyClients办法。

class CustomProxyElement extends ProxyElement {
  CustomProxyElement(ProxyWidget widget) : super(widget);
  @override
  void notifyClients(covariant ProxyWidget oldWidget) {
    //......
  }
}

整个ProxyElement的要害代码,就notifyClients这个函数的完结,它传入了一个老的、支持协变的ProxyWidget进来,这意味着传进来的应该是一个老的CustomProxyWidget的实例,这意味着咱们在notifyClients中,能够一起拿到老的CustomProxyWidget实例当时CustomProxyWidget实例的引用,分别是oldWidgetthis.widget

一新一旧,不难看出ProxyWidget的notifyClients调用,应该是要去做一些新旧Widget的数据比较而存在的

比如,咱们能够这样重写它:

@override
void notifyClients(covariant ProxyWidget oldWidget) {
  if((oldWidget as CustomProxyWidget).data != (widget as CustomProxyWidget).data){
    // 告诉所有订阅者,数据变动了
    _clients.foreach((e)=>e.notify());
  }
}

咱们能够依据data属性(data是CustomProxyWidget新增的一个int类型的字段)的变化,来决议是否需求告诉订阅者的Element是否去从头制作子Widget,一旦data产生了变化,那么就去遍历_clients中的数据,并调用e.notify操作监听者从头制作视图。

这让咱们不由和InheritedWidgetupdateShouldNotify联系起来,简略剖析一下updateShouldNotify的调用链条:

InhertiedElement#update -> updateShouldNotify() 判断是否需求更新数据
InhertiedElement#update -> callsuper 即调用ProxyElement的update办法
ProxyElement#update -> notifyClients();

显然,InheritedWidget将notifyClients做了一个封装updateShouldNotify,并把这个封装放在Widget层,而不是直接让开发者去重写notifyClients这一层,这么做的原因其实和BuildContext存在的含义是相同的,让上层应用开发只重视Widget,而更少地去感知Element的存在

总而言之,notifyClients存在的效果和含义,便是告诉订阅它的子Widget,以完结子Widget的更新,咱们也能稍稍瞥见一些ProxyWidget和ProxyElement的效果,大体上都是和数据传输和同享相关的。

2. InheritedWidget

基于观察者形式的InheritedWidget,它的运用咱们就不做过多的叙述了,全体上而言,就三步走:

  • 注册:运用BuildContext注册监听
  • 经过BuildContext获取数据
  • 告诉:改动促进监听者的数据重绘

这是一个十分典型的观察者形式的运用进程,只不过InheritedWidget为咱们做了一些封装,「注册」、「告诉」操作变得愈加地“荫蔽”了。

2.1 注册

运用InheritedWidget时,咱们并没有手动地调用addListener、addObserver这类的办法,去主动添加监听,这一切都是无感的。咱们一般经过如下办法获取到InheritedWidget中的数据。

context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();

这一行代码已经包括两个进程了:注册监听和获取数据。

InheritedElement傍边,有一个特别的结构,它存储了咱们上面经过context调用时的context,这样来完结注册的监听,并且,在注册完结之后,会将所需求的数据回来给调用者,这样一来,监听注册、数据的获取这一个操作就合二为一了。

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

2.2 告诉

关于StatefulWidget的重绘,咱们一定会想到一个办法:markNeedsBuild(),所以,咱们就顺着上述的调用,查找是否有相关的调用,咱们能够看看归于InheritedElement的notifyClients的调用链:

1. InheritedElement# notifyClients
2. InheritedElement# notifyDependent(oldWidget, dependent);
3.   dependent#didChangeDependencies();

一路从notifyClients调用到_dependents中的某个dependentdidChangeDependencies办法,这便是告诉的整个流程,InheritedWidget经过这样的调用,告诉所有挂载着的监听者,即其他需求InheritedWidget数据的Widget的BuildContext,并调用BuildContext的didChangeDependencies,它的完结如下:

@mustCallSuper
void didChangeDependencies() {
  ……
  markNeedsBuild();
}

至此,InheritedWidget是怎么告诉到子Widget进行更新的整个链路已经是十分清晰了。

由于didChangedDepenedencies()的存在,只要添加了依赖的结点才会由于数据的更新而造成节点的rebuild,而不会像StatefulWidget相同,对整棵子树做一次完全的rebuild,这是整个ProxyWidget/ProxyElement的特性。

2.3 何时更新?

InheritedWidget自身只负责数据的向下传递,子Widget能够从InheritedWidget中读出数据,可是,比如咱们的子Widget中的onPressed的回调函数中,对InheritedWidget中的数据进行修正,通常状况下是无法完结UI的更新的,由于InheritedWidget调用notifyClients()是有机遇约束的。

仅当是ProxyElement#update()被调用时,才会调用updateShouldNotify()去评估是否要调用notifyClients去更新布局。而一般都数据修正,例如int++String赋值等等并不能触发notifyClients调用。

所以,只要Element#update()办法调用时,才干驱动子Widget产生视图更新,而Element#update()办法仅在:Element不变,Widget产生改动的时分才会触发,常见于Widget作为一个装备,产生了改动,而Element产生了复用的状况。比如State调用build办法构建了一个新的Widget子树,这个子树中的Widget都是全新的Widget,并且假如只是修正Text对应的String中的内容,Text对应的Element此刻就会产生复用,这个进程便是Element的update(),即 用新的newWidget替换掉旧的oldWidget的进程,能够了解为Element的装备的改动。

[Flutter] ProxyWidget和Element更新的正确姿势

所以,InheritedWidget的更新就有必要依赖于InheritedWidget的上层更新,比如去调用setState等等,这个触发条件好像有一点苛刻了,咱们肯定是期望在子Widget中修正了InheritedWidget中的数据之后,就直接就能反应到视图。

咱们能够在onPressed等回调办法中,调用完修正办法之后,手动调用一下setState来手动重建Widget,也能够在InheritedWidget中自己界说一个相关的办法,传入Context,统一处理。

3. ParentDataWidget

之前介绍InheritedWidget主要是讲了它作为ProxyWidget,它的notifyClients是怎么完结的,作为ProxyWidget的另一个分支,ParentDataWidget也是一个十分常用的Widget,它的常见完结类包括:Flexible(常用Expanded)Positioned等等。它们都有一个十分明显的特点:具有一个其父组件(Flext、Stack)需求的一个额定信息,父组件会运用这个额定的信息对当时组件进行布局、定位。

相比较于InheritedWidget,ParentDataWidget的运用场景更多的是偏向于视图自身的数据,比如尺度、偏移量等等。

3.1 Positioned

首要咱们来看看Positioned,Stack嵌套Positioned,在Positioned能够设置height/width和left/top/right/bottom等一系列的尺度、位置属性,咱们需求重视的,是ParentDataElement对应的的notifyClients终究干了些什么。

咱们先来看看Positioned的功用。Positioned先将传递进来的renderObject目标中的parentData结构取出,然后再向其中塞数据,之后的布局进程中,Stack就能够依据StackParentData中的数据进行布局了。

ParentDataElement的notifyClients办法,只调用了一个办法,咱们能够快速地定位到_applyParentData办法:

@override
void applyParentData(RenderObject renderObject) {
  assert(renderObject.parentData is StackParentData);
  final StackParentData parentData = renderObject.parentData! as StackParentData;
  ……
}

这里传进来的正是Positioned的child属性对应的RenderObject,Positioned将设置的尺度、偏移量作为一个StackParentData传递进去,然后再Render阶段对其进行位置的确认和布局。

接下来的场景如下:Stack下面套了三个Positioned,对应三个具有色彩的Container。

Positioned自身是不参加Render的,咱们能够很清楚地看到,RenderStack的child直接便是RenderColoredBox,即一个具有色彩的Box,是由Container创立的,而不是一个Positioned(Container自身是一个复合型的StatelessWidget)。咱们能够模糊地了解成,RenderTree下,Stack下直接便是Container。

[Flutter] ProxyWidget和Element更新的正确姿势

ProxyWidget还是会存在于Element、Widget树傍边的,只是在烘托的时分,它并不是一个RenderObject节点,所以,自然而然不参加烘托,可是它的数据还存在它的孩子对应的ParentData傍边。从头构建时,也是调用renderObject.parent(在RenderTree上的parent,即Stack)进行重建

[Flutter] ProxyWidget和Element更新的正确姿势

所以,ProxyWidget自身是不参加烘托的,他只作为一个中心Widget,为下层的Child对应的RenderObject,提供上层(Stack)所需求的数据(尺度、偏移量等等)。

[Flutter] ProxyWidget和Element更新的正确姿势

同为ParentDataWidget的Flexible同理,只不过把适用于Stack的StackParentData,换成了适用于Flex的FlexParentData,以StackParentData为例,咱们只需求知道它的数据是记录在Postioned的child对应的RenderObject下,交给的父布局Stack运用即可,ParentDataWidget的任务也仅限于此

4. 后记

已然Positioned对应的Element也是ProxyElement的子类,那么它的notifyClients的调用就和InheritedWidget相同,当Element#update调用时,才会调用notifyClients,去从头为子Widget设置StackParentData(尺度、宽高数据),然后去从头布局子Widget。

这也是ProxyElement一贯的处理方式,当ProxyWidget对应的数据产生改动(InheritedWidget一般是事务数据,ParentDataWidget一般是一些视图数据),才会去重建视图,而Widget数据产生改动的仅有办法,便是从头创立一个Widget,而不是在原有的Widget上经过回调等手法来进行赋值、增减等等,这种状况并不视为Widget的改动。

从Element的角度来说,假如Widget想要改动就必然要经过Element#update办法,即使是StatefulWidget,它的改动也是从State调用setState开端,然后StatefulWidget去rebuild一个新的Child Widget子树,再调用Element的update办法,将新的子树挂载上来完结新旧数据的更迭。

简略来说,默许状况下,数据的变更有必要准确到Widget层面,Element才有或许看得见。

一旦以为数据产生了改动,那么ProxyElement则会经过notifyClients办法,告诉所有的监听者,监听者此刻的行为:

  • 假如是InheritedWidget,那么便是调用监听者的didChangeDependencies,重建监听者对应的视图。
  • 假如是ParentDataWidget,那么便是调用ParentDataElement的applyParentData函数,去从头build它的子集。

~end