Flutter Widget


从flutter的架构图中不难看出widget是整个视图描述的基础,Flutter 的核心设计思想便是

everything is a widget.

即一切为Widget,与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等,而原生开发中的控件通常只是指UI元素。

Flutter 中的 widget 可以用两条规则来约束:

  1. 一切都是 widget。
  2. 每个 widget 只负责自己关注的部分。


第一条意味着你所看到的东西都是由于 widget 构成,跟原生不同的是,原本在原生中一些参数相关的东西到了 Futter 中都被 widget 化,例如大小、背景、margin、padding 等等原本只需要一个参数设置的东西对应到 Flutter 中都映射成了一个单独的 widget。

第二条的意思是,每个 widget 应该仅负责自己的职责所在,比如文本框 Text 组件,只负责如何显示一个文本,其他一概不管,不用考虑自己的大小、位置、margin 等等,这些由别的专门负责此功能的 widget 来控制,作为一个文本框就只负责文本框的职责。

一. Widget 简介

接下来,我们通过源码(Flutter 2.0.6)及源码的官方文档来进一步理解 Widget,我们先通过官方文档去了解Widget 的描述。

Describes the configuration for an [Element].

由上面的文档可以知道,在Flutter中,Widget的功能是“描述一个UI元素的配置数据”,也就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述Element的一个配置数据(本篇的关注点是 Widget,将不会过多的介绍 Element),告诉 Element 这个实例如何去渲染,接下来我们继续往下看。

/// Widgets are the central class hierarchy in the Flutter framework. A widget
/// is an immutable description of part of a user interface. Widgets can be
/// inflated into elements, which manage the underlying render tree.

Widgets是flutter框架中的最核心的类,一个widget是一部分不可变的用户界面的描述,小部件可以膨胀为元素,用于管理底层渲染树。

/// Widgets themselves have no mutable state (all their fields must be final).
/// If you wish to associate mutable state with a widget, consider using a
/// [StatefulWidget], which creates a [State] object (via
/// [StatefulWidget.createState]) whenever it is inflated into an element and
/// incorporated into the tree.

Widgets 本身没有可变状态(它们的所有字段都必须是final)。如果希望将可变状态与Widgets 关联,请考虑使用
[statefulwidget],它创建一个[状态]对象(通过 [statefulWidget.createState])当它被 inflated 成一个元素时合并到树中

/// A given widget can be included in the tree zero or more times. In particular
/// a given widget can be placed in the tree multiple times. Each time a widget
/// is placed in the tree, it is inflated into an [Element], which means a
/// widget that is incorporated into the tree multiple times will be inflated
/// multiple times.

给定的 widget 可以包含在树中零次或多次。特别地给定的 widget 可以多次放置在树中。每次一个 widget 放在树中,它会膨胀成一个 [element],这意味着多次合并到树中的小部件将被 inflated 多次。

/// The [key] property controls how one widget replaces another widget in the
/// tree. If the [runtimeType] and [key] properties of the two widgets are
/// [operator==], respectively, then the new widget replaces the old widget by
/// updating the underlying element (i.e., by calling [Element.update] with the
/// new widget). Otherwise, the old element is removed from the tree, the new
/// widget is inflated into an element, and the new element is inserted into the
/// tree.

[key]属性控制一个小部件如何替换树中的另一个小部件。如果两个小部件的[runtimeType]和[key]属性都[operator==]相等,则新小部件将通过更新基础元素(使用新小部件调用[element.update])来替换旧小部件。否则,将从树中删除旧元素,将新小部件膨胀为元素,并将新元素插入树中。

整理一下,widget 是用来描述如何创建 Element 的,widget 本身是一个不可变对象,它的所有字段都必须是 final,它可以被复用,请注意,这里的复用不是指在两次渲染的时候将对象从旧树中拿过来放到新树,而是在同一个 Widget Tree 中,某个子 Widget 可以出现多次,因为它只是一个 description。在一次渲染中,Flutter Framework 会调用 Widget的 createElement() 方法,这个方法会创建一个新的对应Element 对象并返回,所以即使 Widget 被重复使用,框架还是会创建多个不同的 Element 对象。

二. Widget 属性和方法

剩下的就是Widget的属性和说明,我们接下来继续往下看,这块将在 Widget 源码里说明

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key? key;
    
  @protected
  @factory
  Element createElement();

  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;

  @override
  @nonVirtual
  int get hashCode => super.hashCode;
  
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  
  static int _debugConcreteSubtype(Widget widget) {
    return widget is StatefulWidget ? 1 :
           widget is StatelessWidget ? 2 :
           0;
    }
}
  • Widget 类继承自 DiagnosticableTree,DiagnosticableTree 即“诊断树”,主要作用是提供调试信息。
  • Key : [key]属性控制一个小部件如何替换树,主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在 canUpdate() 方法中。
  • createElement():正如所述“一个 Widget 可以对应多个 Element ”;Flutter Framework在构建UI树时,会先调用此方法生成对应节点的 Element 对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(…) 复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(…)是一个静态方法,它主要用于在 Widget 树重新 build 时复用旧的 widget,其实具体来说,应该是:是否用新的 Widget 对象去更新旧UI树上所对应的 Element 对象的配置;通过其源码我们可以看到,只要newWidget 与 oldWidget 的 runtimeType 和 key 同时相等时就会用 newWidget 去更新 Element 对象的配置,否则就会创建新的 Element。


另外 Widget 类本身是一个抽象类,其中最核心的就是定义了 createElement() 接口,在 Flutter 开发中,我们一般都不用直接继承 Widget 类来实现一个新组件,相反,我们通常会通过继承 StatelessWidget 或StatefulWidget 来间接继承Widget类来实现。

See also:
///
/// * [StatefulWidget] and [State], for widgets that can build differently
/// several times over their lifetime.
/// * [InheritedWidget], for widgets that introduce ambient state that can
/// be read by descendant widgets.
/// * [StatelessWidget], for widgets that always build the same way given a
/// particular configuration and ambient state.

_/// [StatefulWidget] and [State],_在其生命周期内可以多次进行不同构建的小部件
/// _[InheritedWidget], _引入可被后代 widget 读取的环境状态的 widget。
_/// [StatelessWidget], _总是以相同的方式构建特定配置和环境状态的小部件


StatelessWidget 和 StatefulWidget 都是直接继承自Widget类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 Widget 模型,后续我们将重点一一介绍这两个类.


三. Widget key

在每个widget的构造函数都有一个key参数,这个参数的作用是什么呢?**Key用于在widget的位置改变时保留其状态。**比如,保留用户的滑动位置,或者在保留widget状态的情况下修改一个widget集合,如Row、Column等。

3.1 Key是什么?
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.

Key是Widget、Element和SemanticsNode的标识符。

只有当一个新的小部件的键与当前该元素关联的小部件的键相同时,它才会用于更新现有的元素。
Flutter中常见的Key有:

- LocalKey
  - ObjectKey
  - UniqueKey
  - ValueKey
    - PageStorageKey
- GlobalKey
  - GlobalObjectKey
  - LabeledGlobalKey
3.2 何时需要使用Key?

大多数时候不需要,不过当需要在一个相同类型的、有状态的widget集合中添加、删除或调整顺序时,就需要使用Key了。比如,在一个待办事项App中,我们需要可以执行添加新事项、根据优先级调整事项顺序、在完成事项后移除它们等操作。

先通过一个简单的例子来了解一下为什么需要使用Key。下面是一个随机数列表:

在这里插入图片描述

RandomNum是一个StatelessWidget:

class RandomNum extends StatelessWidget {
  final int num;
  RandomNum(): num = Random().nextInt(1000 * 1000), super();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(8),
      child: Text('$num'),
    );
  }
}

当我们点击Reorder按钮时,随机数列表会重新排序,一切正常。然后我们将RandomNum改为StatefulWidget。

class RandomNum extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => RandomNumState();
}

class RandomNumState extends State<RandomNum> {
  int num;

  @override
  void initState() {
    num = Random().nextInt(1000 * 1000);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(8),
      child: Text('$num'),
    );
  }
}

此时,再点击 Reorder 按钮,屏幕上的数字没有变化。怎么回事呢?

我们知道,在 Flutter 中,每个 Widget 都对应一个 Element。Element 树其实是非常简单的,仅保存了关联的 widget 的类型以及指向子 element 的连接。可以将 element 树看作 app 的骨架,它展示了app的结构,但是所有的附加信息需要通过指向widget的引用来查找。

在这里插入图片描述

这是无状态版本的 widget 和 element 树。当我们改变 items 的顺序时,Flutter会遍历 element 树以确认结构是否改变,从 Column 对应的 element 开始,一直到所有的子孙结点。对于每个 element,Flutter 会检查新 widget 的类型和 key 与当前引用的 widget 的类型和 key 是否一致,如果一致就将引用指向新的 widget。对于RandomNum 来说,由于没有 key ,因此 Flutter 只会检查其类型。


当我们改变 items 的顺序后,widget 树会重建,但由于结构与之前一致,所以 element 树结构并不会改变,只不过 element 指向的 widget 引用发生了变化。对于 StatelessWidget 来说,这并没有什么问题,所有的信息都保存在 widget 中,只要改变 widget 树就可以了。

在这里插入图片描述


当 RandomNum 变为 StatefulWidget 后,widget 树、element 树与之前一样,但是增加了关联的 State 对象。此时,num 不再保存在 widget 中,而是保存在 State 对象中。当我们调整 widget 的顺序后,Flutter 依然会遍历 element 树,检查树结构是否改变,并更新指向 widget 的引用。

Flutter根据element树以及关联的State对象来确定屏幕上的实际显示内容。因此,屏幕显示内容不会改变。现在,我们给RandomNum增加一个Key:

class _RandomNumAppState extends State<RandomNumApp> {
  List<RandomNum> items;
  
  @override
  void initState() {
    items = [
      RandomNum(key: UniqueKey()),
      RandomNum(key: UniqueKey()),
      RandomNum(key: UniqueKey()),
      RandomNum(key: UniqueKey()),
      RandomNum(key: UniqueKey()),
      RandomNum(key: UniqueKey()),
    ];
    super.initState();
  }
  ...
}

class RandomNum extends StatefulWidget {
  RandomNum({Key key}): super(key: key);

  @override
  State<StatefulWidget> createState() => RandomNumState();
}

在这里插入图片描述


当我们改变 items 的顺序时,Flutter 遍历 element 树以检查是否需要更新。Column 与之前一样,直接将 widget 引用指向新的 widget 即可。对于 RandomNum 的 element 来说,由于 element 的 Key 值与对应 widget 的 Key 值不同,因此 Flutter 会使这个 element 暂时失效,并移除对它的引用以及其对 widget 的引用。
在这里插入图片描述


然后,从第一个不匹配的widget开始,Flutter会在所有失效的子结点中查找具有对应Key值的element,如果找到了,则将这个element的widget引用指向到这个widget。接着对第二个widget做相同的操作,以此类推。

在这里插入图片描述


当遍历结束之后,更新element树的引用,此时widget、element、state就对应起来了。

在这里插入图片描述


总结来说,当我们需要更新一个有状态的、同类型的widget组成的集合时需要使用Key来保留widget的状态。

3.3 应该在何处使用Key?

当我们需要使用Key时,应该将Key用在widget树的什么位置呢?**需要保存状态的widget子树的顶层。**我们刚才一直在谈论state,你或许会认为应该用在第一个StatefulWidget上,不过这是错误的!!

将上面的例子略作修改,我们将RandomNum这个StatefulWidget包裹在一个Padding里面:

class _RandomNumAppState extends State<RandomNumApp> {
  List<Padding> items;

  @override
  void initState() {
    items = [
      Padding(
        padding: EdgeInsets.all(4),
        child: RandomNum(key: UniqueKey()),
      ),
      Padding(
        padding: EdgeInsets.all(4),
        child: RandomNum(key: UniqueKey()),
      ),
      Padding(
        padding: EdgeInsets.all(4),
        child: RandomNum(key: UniqueKey()),
      ),
      Padding(
        padding: EdgeInsets.all(4),
        child: RandomNum(key: UniqueKey()),
      ),
    ];
    super.initState();
  }
}

再次运行程序,我们发现所有的数字每次都重新生成了一遍,怎么回事呢?先来看一下添加Padding之后的widget和element树。
在这里插入图片描述


当我们改变RandomNum结点的位置时,Flutter的widget-to-element匹配算法每次在树中查找一级。我们先看第一层,即Padding层,暂时忽略其它结点,每次只看一层。

在这里插入图片描述


可以看到,对于Padding这一层来说,调整RandomNum的顺序之后,匹配关系并没有发生什么变化,Padding还没有Key,因此只需要比较其类型,显然类型都一样,更新element指向widget的引用即可。
然后来看第二层,即第一个Padding对应的子树:

在这里插入图片描述


Element的key值与widget的key值不匹配,因此Flutter会使这个element失效并移除对它的连接。我们在例子中使用的是本地Key,这意味着Flutter只会在一个层级中使用这个Key值来匹配widget和element。由于无法在同级找到拥有相同Key值的element,因此Flutter会重新创建一个新的element并赋予新的State对象。所以,我们看到所有的数字都重新创建了。

如果我们将Key用在Padding上,Flutter就会感知到变化并正确地更新连接,就像上面的例子一样。

class _RandomNumAppState extends State<RandomNumApp> {
  List<Padding> items;

  @override
  void initState() {
    items = [
      Padding(
        key: UniqueKey(),
        padding: EdgeInsets.all(4),
        child: RandomNum(),
      ),
      Padding(
        key: UniqueKey(),
        padding: EdgeInsets.all(4),
        child: RandomNum(),
      ),
      Padding(
        key: UniqueKey(),
        padding: EdgeInsets.all(4),
        child: RandomNum(),
      ),
      Padding(
        key: UniqueKey(),
        padding: EdgeInsets.all(4),
        child: RandomNum(),
      ),
    ];
    super.initState();
  }
  ...
}

在这里插入图片描述


3.4 应该使用什么类型的Key?

我们已经知道何时需要使用Key,以及应该在何处使用Key。不过,如果我们看一下Flutter的文档,就会发现有很多类型的Key,那么应该使用什么类型的Key呢?

当我们修改一个widget集合时,就像上面的将一组数字重新排序那样,只需要与其它widget的key区分开来即可。对于这种情况,可以根据widget中保存的信息来做选择。

  • LocalKey

当我们修改一个widget集合时,就像上面的将一组数字重新排序那样,只需要与其它widget的key区分开来即可。对于这种情况,可以根据widget中保存的信息来做选择。

  • ValueKey

相等性由其value属性确定。
在一个待办事项列表中,如果每个列表项的文字是唯一的,那么ValueKey是不错的选择,将文字作为其值:

return TodoItem(
  key: ValueKey(todo.task),
  todo: todo,
  onDismissed: (direction) {
    _removeTodo(context, todo);
  },
);
  • ObjectKey

相等性由其Object类似的value属性确定。
如果widget中保存的是复杂信息的组合呢?比如在一个通讯录App中,每个人的信息有很多项:

AddressBookEntry:
  FirstName: Hob
  LastName: Reload
  Birthday: July 18
  
AddressBookEntry:
  FirstName: Ella
  LastName: Mentary
  Birthday: July 18
  
AddressBookEntry:
  FirstName: Hob
  LastName: Thyme
  Birthday: February 29

任何一项信息可能都不是唯一的,姓名、出生日期都可能重复,不过信息的组合是唯一的。对于这种情况,ObjectKey或许是最合适的。

  • UniqueKey

只与自己相等。
如果多个widget的值相同,或者想要确保每个Key的唯一性,可以使用UniqueKey。在上面的例子中使用的就是UniqueKey,因为我们没有在widget中保存任何不变且唯一的数据,数字需要等到RandomNum构建或initState时才能确定。

  • PageStorageKey

定义PageStorage的值存放在何处的ValueKey。
滚动列表(ScrollPosition)使用PageStorage保存滚动位置,每次滚动停止时都会更新PageStorage中保存的值。

void didEndScroll() {
  activity.dispatchScrollEndNotification(copyWith(), context.notificationContext);
  if (keepScrollOffset)
    saveScrollOffset();
}

@protected
void saveScrollOffset(){
  PageStorage.of(context.storageContext)?.writeState(context.storageContext, pixels);
}

PageStorage用于保存和恢复比widget生命周期长的数据,这些数据保存在一个per-route的Map中,Key由widget的PageStorageKey和其祖先结点来决定。要使widget重建时能够找到保存的值,key值的identity在每次widget构建时必须保持不变。

例如,为了确保TabBarView重建时每个MyScrollableTabView的滚动位置都能被恢复,我们为其指定了PageStorageKey:

TabBarView(
  children: myTabs.map((Tab tab) {
    MyScrollableTabView(
      key: PageStorageKey<String>(tab.text), // like 'Tab 1'
      tab: tab,
    ),
  }),
)
  • GlobalKey

在整个App中唯一的Key。
GlobalKey唯一标识了一个element,通过GlobalKey可以访问与此element关联的其它对象,比如BuildContext。对于StatefulWidget来说,可以通过GlobalKey访问其State。
与上面介绍的LocalKey不同,含有GlobalKey的widget在移动位置时可以改变父结点。与LocalKey相同的是,位置的移动必须在一个动画帧内完成。
例如,我们想在不同的页面展示同一个widget,同时保持其状态,就需要使用GlobalKey。
GlobalKey的成本比较高,如果不是为了上面的两个目的,即在保持widget状态的情况下更换父节点,或者需要访问在widget树中完全不同部分的widget中的信息,可以考虑使用上面介绍的LocalKey。

当在widget树中更换位置时,使用Key来保持状态。最常见的场景是修改一个相同类型widget组成的集合,例如一个列表。将Key放在希望保持其状态的widget子树的顶部,根据widget中保存的信息类型选择合适的Key。



参考资料

  1. Widget简介
  2. Widget基础系列 - Key
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值