开发者

ProxyWidget和Element更新的正确方式详解

开发者 https://www.devze.com 2023-01-19 10:17 出处:网络 作者: 开中断
目录正文1. ProxyWidget和ProxyElement的主要功能2. Inheritedwidget2.1 注册2.2 通知2.3 何时更新?3. ParentDataWidget3.1 Positioned4. 后记正文
目录
  • 正文
  • 1. ProxyWidget和ProxyElement的主要功能
  • 2. Inheritedwidget
    • 2.1 注册
    • 2.2 通知
    • 2.3 何时更新?
  • 3. ParentDataWidget
    • 3.1 Positioned
  • 4. 后记

    正文

    Flutter的众多Widget当中,有作用于渲染的RenderObjectWidget、聚焦于功能整合的StatefulWidget。但是,还有一个大类,ProxyWidget也同样值得我们关注。

    ProxyWidget和Element更新的正确方式详解

    与其相关的有两个大类:

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

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

    所以,我们可以根据ProxyWidget的子类,向上预先给ProxyWidget扣一个数据共享的「帽子」。

    1. ProxyWidget和ProxyElement的主要功能

    ProandroidxyWidget本身是抽象的,需要我们重写它的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实例和当前CustojavascriptmProxyWidget实例的引用,分别是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的调用链:

     InheritedElement# notifyClients
     InheritedElement# notifyDependent(oldWidget, dependent);
     dependent#didChangeDependencies();
    

    一路从notifyClients调用到_dependents中的某个dependentdidChangeDependencies方法,这就是通知的整个流程,InheritedWidget通过这样的调用,通知所有挂载着的监听者,即其他需要InheritedWidget数据的Widget的BuildContext,并调用BuildContext的didChangeDependencies,它的实现如下:

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

    至此,InheritedWidget是如何通知到子Widget进行更新的整个链路已经是非常清晰了。

    由于didChangedDepenedencies()的存在,只有添加了依赖的结点才会因为数据的更新而造成节点的rebuild,而不会像Statefu开发者_Js入门lWidget一样,对整棵子树做一次完全的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的配置的改变。

    ProxyWidget和Element更新的正确方式详解

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

    我们可以在onPressed等回调方法中,调用完修改方法之后,手动调用一下setState来手动重建Widget,也可以在InheritedWidget中自己定义一个相关的方法,传入Context,统一处理。

    3. ParentDataWidget

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

    相比较于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更新的正确方式详解

    ProxyWidget还是会存在于Element、Widget树当中的,只是在渲染的时候,它并不是一个RenderObject节点,所以,自然而然不参与渲染,但是它的数据还存在它的孩子对应的ParentData当中。重新构建时,也是调用renderObject.parent(在RenderTree上的parent,即Stack)进行重建

    ProxyWidget和Element更新的正确方式详解

    所以,ProxyWidget本身是不参与渲染的,他只作为一个中间Widget,为下层的Child对应的RenderObject,提供上层(Stack)所需要的数据(尺寸、偏移量等等)。

    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的applyParenhttp://www.devze.comtData函数,去重新build它的子集。

    以上就是ProxyWidget和Element更新的正确方式详解的详细内容,更多关于ProxyWidget Element更新的资料请关注我们其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号