ProxyWidget和Element更新的正确方式详解
开中断 人气:0正文
Flutter的众多Widget当中,有作用于渲染的RenderObjectWidget、聚焦于功能整合的StatefulWidget。但是,还有一个大类,ProxyWidget也同样值得我们关注。
与其相关的有两个大类:
- 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实例的引用,分别是oldWidget
和this.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
操作监听者重新绘制视图。
这让我们不禁和InheritedWidget
的updateShouldNotify
联系起来,简单分析一下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
的调用链:
InheritedElement# notifyClients InheritedElement# notifyDependent(oldWidget, dependent); dependent#didChangeDependencies();
一路从notifyClients
调用到_dependents
中的某个dependent
的didChangeDependencies
方法,这就是通知的整个流程,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的配置的改变。
所以,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。
ProxyWidget还是会存在于Element、Widget树当中的,只是在渲染的时候,它并不是一个RenderObject节点,所以,自然而然不参与渲染,但是它的数据还存在它的孩子对应的ParentData当中。重新构建时,也是调用renderObject.parent(在RenderTree上的parent,即Stack)进行重建
所以,ProxyWidget本身是不参与渲染的,他只作为一个中间Widget,为下层的Child对应的RenderObject,提供上层(Stack)所需要的数据(尺寸、偏移量等等)。
同为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它的子集。
加载全部内容