您的位置:首页 > 教程 > Android开发 > ProxyWidget和Element更新的正确方式详解

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

2023-01-18 10:08:04 来源:易采站长站 作者:

目录正文1.ProxyWidget和ProxyElement的主要功能2.InheritedWidget2.1注册2.2通知2.3何时更新?3.ParentDataWidget3.1Po...

目录
正文
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实例和当前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的调用链:

 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,而不会像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的配置的改变。

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.cppcns.comtData函数,去重新build它的子集。

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

如有侵权,请发邮件到 [email protected]

相关文章

  • Android中RecyclerView实现多级折叠列表效果(TreeRecyclerView)

    Android中RecyclerView实现多级折叠列表效果(TreeRecyclerView)

    前言 首先不得不吐槽一下产品,尼玛为啥要搞这样的功能....搞个两级的不就好了嘛...自带控件,多好。三级,四级,听说还有六级的....这样丧心病狂的设计,后台也不好给数据吧。 先看
    2019-12-15
  • Android Viewpager实现无限循环轮播图

    Android Viewpager实现无限循环轮播图

    在网上找了很多viewpager实现图片轮播的,但是大多数通过以下方式在PagerAdapter的getCount()返回一个无限大的数,来实现 伪无限 @Override public int getCount() { return Integer.MAX_VALUE;//返回一个无限
    2019-12-22
  • AndroidStudio4.0 New Class的坑(小结)

    AndroidStudio4.0 New Class的坑(小结)

    AndroidStudio升级后, 新建类变成了下面这个样子, 并且默认还没有修饰符, 一堆大写的英文看的我一脸懵逼, 后来在 stackoverflow 上看到了这几个参数的含义: IMPORT BLOCK 说白了就是导包(谁家导
    2020-07-01
  • Android自定义View实现通讯录字母索引(仿微信通讯录)

    Android自定义View实现通讯录字母索引(仿微信通讯录)

    一、效果:我们看到很多软件的通讯录在右侧都有一个字母索引功能,像微信,小米通讯录,QQ,还有美团选择地区等等。这里我截了一张美团选择城市的图片来看看; 我们今天就来实现
    2019-12-16
  • Android个人手机通讯录开发详解

    Android个人手机通讯录开发详解

    一、Android 个人手机通讯录开发 数据存储:SQLite 数据库 开发工具:Android Studio 二、Phone Module 简介 1. 界面展示 2. 文件结构简单分析 三、个人手机通讯录代码实现 1. 清单文件 (AndroidMa
    2019-12-24
  • Android10.0实现本地音乐播放(附源码下载)

    Android10.0实现本地音乐播放(附源码下载)

    1.概述 本篇文章仅是Android小白在写一个小程序,内容仅供参考,有很多不足之处希望各位大神指出,文章末尾有整个项目的下载,不需要币,只求帮你们解决到问题的同时收获到一颗小
    2020-06-23
  • Android使用VideoView播放本地视频和网络视频的方法

    Android使用VideoView播放本地视频和网络视频的方法

    1、效果展示 2、布局文件 ?xml version=1.0 encoding=utf-8?RelativeLayout xmlns:android=http://www.easck.com/apk/res/android xmlns:tools=http://www.easck.com/tools android:layout_width=match_parent android:layout_height=match_parent
    2019-12-13
  • Android实现简易计步器功能隔天步数清零查看历史运动纪录

    Android实现简易计步器功能隔天步数清零查看历史运动纪录

    最近需要用到计步功能,这可难坏我了,iOS端倒好,有自带的计步功能,让我惊讶的是连已爬楼层都给做好了,只需要调接口便可获得数据,我有一句MMP,我很想讲。 但是抱怨归抱怨,
    2019-12-21