Flutter学习 Widget简介

目录

1. Widget 概述

1.1 Widget概念

在 Flutter 中,几乎所有的对象都是一个 Widget ,与原生的“控件”的,Flutter 中的 Widget 是一个更广泛的概念,正所谓一切皆可Widget, 它不仅可以表示 UI 元素,也可以表示一些功能性的组件,例如 Theme、GuestureDector等。

Flutter 的 Widget 其实就是 “组件”、“部件”、“控件”的概念, 因为其实际灵感是来源于 React, 所以其目标就是通过 Widget 嵌套 Widget 的方式来构建UI和进行逻辑处理。

和 Android 的View相比,Widget 粗略的可以相当于View, Widget 和 View最大的不同是:Widget具有不同的生命周期,每当 Widget 或其状态状态发生变化时, Flutter 的框架都会创建一个新的 Widget实例树, 相比之下,Android 中的 View 会被绘制一次,并且在 invalidate 调用之前不会重绘。

1.2 Widget 分类

因为万物皆可 Widget, 所以 Widget 承载了基本所有的业务,自然而然也有各种各样的Widget,分类也有很多,主要包括下面这些类别:

  • Basics: 基础组件,例如 Text、Button等
  • Material Components: 具有 Material Design 风格的组件
  • Cupertino:iOS风格组件
  • Accessibility: 辅助功能的组件
  • Animation:动画组件
  • Scrolling:滚动组件
  • Layout:布局组件
  • Async:异步组件

Basics 比较特殊, 它并不是一个专门的类别组件,而是从其他官方Widget类中,选取一些常用的、易用的组件组成的类别,例如 Row 属于 Layout 组件的东西,但它也被选进了 Basics。

所以官方的意图是,在你开始构建第一个 Flutter 应用前,你可以通过学习 Basics 基础组件,来了解一些最常用的开发组件和知识。

Widget 更多的是以组合的形式存在,这其实体现良好的设计思想,因为在很多场景中,组合的设计结构是要比继承的结构好的。
例如 Container 是属于 Layout组件中的一个 Widget, 而 Container 又有 LimitedBox、ConstrainedBox、Aligin、Padding、DecoratedBox、 Transform 等部件来组成。如果想要实现 Container 的自定义效果,可以组合上面这些 Widget 以及其他简单的 Widget, 而不是把它写成某个Layout组件的子类,这样做的好处是:

  1. 这样不会限制它的行为
    类比 Android,一个实现了复杂的效果的 Button视图 如果是继承的 FrameLayout,你会觉得它被限制了很多行为,它看起来是一个 Button,但它的逻辑却是一个 Layout
  2. 少写胶水代码
    例如上一点,本来想要给 Button 设置一个 Text,但是 FrameLayout 没有 setText方法,只能写这种胶水代码,来调用 Button 的 setText 方法

2. Widget 接口

在 Flutter 中, Widget 的功能是 “描述一个 UI 元素的配置信息”,也就是说 Widget 并不是表示最终绘制在设备屏幕上的显示元素,比如对 Text 来讲,文本的内容、文本样式等都是他的配置信息,来通过下面 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;
  }
  ...
}
  • @immutable
    代表 Widget 是不可变的, 这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许Widget中定义的属性变化呢? 这是因为 Widget 中的属性发生变化,Flutter会重新构建 Widget 树来替换旧的 Widget树,相当于自己的属性变了,自己就会被替换,这是无意义的。
  • Widget 类是继承自 DiagnosticableTreeDiagnosticableTree 即 “诊断树”,主要作用是提供调试信息
  • Key:类似于 React/Vue 中的 key,主要的作用是决定是否在下一次 build 时复用旧的 Widget,决定的条件在 canUpdate方法中
  • createElement()
    一个 Widget 有多个 Element, Flutter 框架在构建 UI 树时,会先调用此方法生成对应节点的 Element 对象。此方法是 Flutter 框架隐式调用的, 在我们开发过程中基本不会调用到
  • debugFillProperties()
    复写父类的方法,主要是设置诊断树的一些特性
  • canUpdate()
    是一个静态方法,他主要用于在 Widget 树重新 build 时复用旧的 Widget, 具体来说,应该是:是否用新的 Widget 对象去更新旧 UI 树上所对应的 Element 对象的配置, 通过源码我们可以看到,只要新旧 Widget 的 runtimeType 和 key相等时,就会用 newWidget 去更新 Element 对象的配置,否则就会创建新的 Element。

Widget 本身是一个抽象类,其中最核心的就是定义了 createElement() 接口。在 Flutter 开发中,我们不会直接继承 Widget 类来实现组件,而是继承 StatelessWidget 或者 StatefulWidget 来间接继承 Widget 类。接下来来重点介绍这两个类。

3. StatelessWidget 和 StatefulWidget

3.1 Flutter 中的四棵树

来看看 Flutter 框架的处理流程:

  1. 根据 Widget 树生成一个 Element 树, Element 树中的节点都继承自 Element
  2. 根据 Element 树生成 Render 树(即渲染树), 渲染树中的节点都继承自 RenderObject
  3. 根据 渲染树 生成 Layer 树 ,然后上屏显示, Layer树中的节点都继承自 Layer

也就是说,真正的布局和渲染逻辑在 Render树中, Element 是 Widget 和 RenderObject 的中间态,用下面例子来说明,假设有一个 Widget 树:

Container( // 一个容器 widget
  color: const Color.fromRGBO(0, 0, 100, 1), // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 显示图片的 widget
      const Text('A'),
    ],
  ),
);

如果 Container 设置了背景色, Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:

if (color != null)
  current = ColoredBox(color: color!, child: current);

Image 内部会通过 RawImage 来渲染图片、 Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、 Element树、渲染树如下图所示:
Flutter学习 Widget简介
这里需要注意的是:

  • Widget 树 和 Element 树是一一对应的,但是和渲染树并不是。 比如 StatelessWidgetStatefulWidget 都没有对应的 RenderObject
  • 渲染树在上屏前会生成 Layer 树,这个会在后面的原理讲到

3.2 StatelessWidget

StatelessWidget 继承自 Widget 类,重写了 createElement() :

@override
StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自 Element 类, 与 StatelessWidget 是对应的。

StatelessWidget 的作用域是不需要维护状态的场景,它通常在 build 方法中通过嵌套其它 Widget 来构建UI,在构建过程中会递归的构建其嵌套的 Widget。 也就说它的一个主要场景是作为根布局容器。

来看下面一段官方代码:

class Echo extends StatelessWidget  {
  const Echo({
    Key? key,  
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  @override
  widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

上述代码实现了一个显示字符串的 Widget。

这里有几个注意的点:

  • Widget 的构造函数必须要的传参要加入 requeired 关键字
  • 在继承 Widget时,通常第一个参数是 Key
  • 如果 Widget需要接受子Widget, 那么 childchildren 参数通常应被放在参数列表的最后
  • Widget 的属性应尽可能的被声明为 final, 防止意外被改变

然后我们可以在别的 Widget 里面通过如下方式使用它:

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

如下所示:
Flutter学习 Widget简介

3.2.1 Context

build() 中有一个 BuildContext 的传参,它是 BuildContext 类的一个实例,表示当前 Widget 在 Widget 树中的上下文,每一个 Widget 都有一个 Context对象。 实际上 context 是当前Widget 在 Widget 树中位置执行“相关操作”的一个句柄, 比如它提供了从当前 Widget 开始向上遍历 Widget 树以及按照 Widget 类型 查找父级 Widget 的方法。 下面是在 子树中获取父级 Widget 的一个示例:

class ContextRoute extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

3.3 StatefulWidget

StatefulWidget 也是继承了 Widget 类, 并重写了 createElement() 方法, 它返回的是一个 StatefulEment 对象。 另外 StatefulWidget 添加了一个新的接口 createState():

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key? key }) : super(key: key);

  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  @factory
  State createState(); 
  • StatefulElment 间接继承自 Element 类, 与 StatefulWidget 对应。 StatefulElement 中可能会多次调用 createElement 来创建状态对象
  • createState 用于创建和 StatefulWidget 相关的状态,它在 StateWidget 的生命周期中可能会被多次调用。
    例如, 当一个 StatefulWidget 同时插入到 Widget 树的多个位置时, Flutter 框架就会调用该方法为每一个位置生成独立的 State实例,本质上一个 StatefulElement 对应一个 State 实例

在 StatefulWidget 中, State 对象和 StatefulElement 具有一一对应的关系。所以在 Flutter 的 SDK 中,经常能看到注释:“从树中移除 State 对象” 或 “插入 State 对象”, 这里的树指的就是 Element 树。

3.4 State

State 表示的是预期对应的 StatefulWidget 要维护的状态, State中的保存的状态信息可以:

  1. 在 Widget 构建时可以被同步读取
  2. 在 Widget 生命周期中可以改变,改变时, 可以手动调用其 setState() 方法通知 Flutter 框架状态发生改变, Flutter 框架在接收到消息后,会重新调用 StatefulWidget.build 重新构建 Widget 树,已达到更新 UI 的目的

State 中两个常用属性:

  1. widget,它表示与该 State 实例关联的 Widget实例 。 需要注意的是,这种关联不是永久的,因为 State 的实例只有在第一次插入树中会被创建, 而 StatefulWidget 因为改变,其实例会被多次创建, 那么 State.widget 就会被动态设置为新的 Widget
  2. context, 就是 BuildContext

3.4.1 State 的生命周期

State 的生命周期对理解 Flutter 是非常重要的。下面来通过官方的例子来学习 State 的生命周期。

实现一个计数器的功能 CounterWidget 组件, 点击可以使得计数+1,由于要保存计数器的数值状态,所以我们应继承 StatefulWidget,代码如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;

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

CounterWidget 接受一个 initValue 的整型,它表示计数器的初始值,而 createState() 方法则创建一个 CounterWidgetState 的 State,用于绑定该 Widget ,来看下 State 的代码:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _counter = widget.initValue;
    print("init State :$_counter");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text("$_counter"),
          // 点击事件, 点击后自增
          onPressed: () =>
              setState(() {
                ++_counter;
              }),
        ),
      ),
    );
  }
  
  @override
  void didUpdateWidget(covariant CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }
  
  @override
  void deactivate() {
    super.deactivate();
    print("deactivate");
  }
  

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }
  
  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

接下来使用初始页来打开一个新路由,在新路由里面只显示这个 Widget,新打开页面后,日志会输出:

Flutter学习 Widget简介
在 StatefulWidget 插入到 Widget 树时, State 的 initState() 会被调用

然后点击 ⚡️ 按钮热重载,控制台会输出下面的日志:

reassemble
deactive
dispose

在 Counter 从 Widget 树中移除时, deactviedispose 会被依次调用,下面来看看各个回调函数:

  • initState()
    当 Widget 第一次插入到 Widget 树时会被调用。 对于每一个State对象,Flutter只会调用一次该回调,通常都是在该回调中做一次性的操作,例如状态初始化、订阅子树的事件通知等。
  • didChangeDependencies()
    当 State 对象的依赖发生变化时会被调用,例如:在之前 build() 中包含了一个 InheritedWidget,然后在之后的 build() 中的 InheritedWidget发生了变化,那么此时 Inherited Widget 的子 Widget 的 didChangeDependencies() 回调都会被调用。
    例如系统语言Locale、主题改变时,就会调用该回调通知。
  • build()
    主要用于构建 Widget 子树。会在如下场景被调用:
    ①:调用 initState()
    ②:调用 didUpdateWidget()
    ③:调用 setState()
    ④:调用 didChangeDependencies()
    ⑤:在 State对象从树中一个位置移除后又重新插入到树的其它位置之后
  • reassemable()
    专门为开发调试使用的, 仅在 热重载 时会被调用,在 Release 下永远不会被调用
  • didUpdateWidget()
    在 Widget 重新构建时, Flutter 框架会调用 Widget.canUpdate() 来检测 Widget 树中同一位置的新旧节点,然后决定是否需要更新,如果 Widget.canUpdate 返回 true,则会调用该回调。
  • deactiveate()
    当 State 对象从树中被移除时,会调用此回调,在一些场景下, Flutter 框架会将 State 对象重新插入到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时。 如果移除后没有重新插入到树中会紧接着调用 dispose()
  • dispose()
    当 State 对象从树中被永久移除时调用,一般在这个回调中释放资源。

StatefulWidget 的生命周期图如下所示:
Flutter学习 Widget简介

3.4.2 build 方法为什么在 State 中而不是在 StatefulWidget 中

前面介绍过, StatelessWidget 中是有 build() 方法中,但与之对应的 StatefulWidget 却把 build() 方法放在了 State中,这是为什么呢?

这主要是为了提高开发的灵活性,如果将 build() 放在 StatefulWidget 主要有两个问题:

  1. 状态访问不便
    假如我们的 StatefulWidget 有很多的状态,而每次状态改变都要调用 build(),由于状态是放在 State 中的,那么 build 和 State 放在两个类别中,构建时读取状态会很不方便。
    并且需要把 State 设置为公开状态,这会导致状态不再具有私密性,导致其修改会不可控。
  2. 继承 StatefulWidget 不便
    子类继承 StatefulWidget 类,意味着要做状态传递,做状态传递是毫无意义的,具体可以参考:为什么不将 build 方法放在StatefulWidget上

3.4.3 在 Widget 树中获取 State 对象

StatefulWidget 的逻辑都都是在其 State 中,所以很多时候,需要获取 StatefulWidget.State 对象来调用一些方法是,比如 Scaffold 组件打开 SnackBar 的逻辑就是放在其 State:ScaffoldState 中的。

我们有两种方法在 子 Widget 树中获取 父级 StatefulWidget 的State 对象。

3.4.3.1 通过 Context获取

有一个 context.findAncestorStateOfType() 方法,该方法可以从当前节点沿着 Widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象,下面是实现打开 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );}),],),),
      drawer: Drawer(),
    );
  }
}

一般来说, 如果 StatefulWidget 的状态是私有的,那么就不应该去直接获取其 State 的对象,因为其不希望被暴露出来。
相反的,如果 StatefulWidget 的状态是暴露出来的,我们就可以去获取。

但通过 context.findAncestorStateOfType() 获取 StetefulWidget 的状态的方法是通用的,我们并不能在语法层面上指定 StatefulWidget 的状态是否为私有。

所以在Flutter开发中有一个潜规则:如果 StatefulWidget 的状态是希望暴露出来的,应该在 StatefulWidget 中提供一个 of() 的静态方法来获取其 State 对象,开发者可以直接通过该方法来获取,如果不希望暴露,则不提供该方法

Scaffold 也提供了一个 of 方法,我们可以直接调用它:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
    },
    child: Text('打开抽屉菜单2'),
  );
}),

3.4.3.2 通过 GlobalKey 获取

通过 GlobalKey 来获取也是一个常用的方式,步骤为:

  1. 给目标 StatefulWidget 添加 GlobalKey:
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)
  1. 通过 GlobalKey 来获取 State 对象
_globalKey.currentState.openDrawer()

GlobalKey 其实是 FLutter 提供的一种整个 App 中应用 element 的机制, 如果一个 Widget 设置了 GlobalKey, 我们可以通过

  • GlobalKey.currentWidget 获取该 Widget 对象
  • GlobalKey.currentElement 获取该 Widget 对应的 Elment 对象
  • GlobalKey.currentState 获取该 Widget 的 State 对象,前提是 StatefulWidget

3.5 通过 RenderObject 自定义 Widget

StatelessWidget 和 StatefulWidget 都是用于组合组件的, 他们本身没有对应的 RenderObject

Flutter 库中很多基础组件都不是通过 StatelessWidget 和 StatefulWidget 实现的, 例如 Text、Colume、Align。他们都是积木,“元组件”,而这些元组件都是通过自定义 RenderObject 来实现的

实际上 Flutter 最原始定义组件的方式就是通过定义 RnederObject 来实现, 用官方示例来简单演示一下通过 RenderObject 定义组件的方式:

class CustomWidget extends LeafRenderObjectWidget{
  @override
  RenderObject createRenderObject(BuildContext context) {
    // 创建 RenderObject
    return RenderCustomObject();
  }
  @override
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 实现布局逻辑
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

如果组件不会包含子组件,则可以直接继承 LeafRenderObjectWidget, 它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承 子Widget,如下所示:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  const LeafRenderObjectWidget({ Key? key }) : super(key: key);

  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

它返回的 Element 是一个 LeafRenderObjectElement,如果自定义的 Widget 可以包含子组件,则可以根据子组件的数量来选择继承 SingleChildRenderObject 或者 MultiChildRenderObjectWidget

  • createRenderObject() ,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用用于生成渲染对象。 我们的主要任务就是实现这个方法,返回渲染对象类的, 本例中是 RenderCustomObject
  • updateRenderObject()
    用于在组件树状态发生变化,单不需要重新创建 RenderObject 时用于更新组件渲染对象的回调

RenderCustomObject 类是继承 RenderBox, 而 RnederBox 继承自 RenderObject我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑,关于如何实现这些逻辑,以后会讲到。

参考

官方文档
Flutter基础四

上一篇:Java 多线程启动的基础方式


下一篇:关于装饰模式的一次实践