乐趣区

Flutter开发之基础Widgets

Widgets 概念

Flutter 里有一个非常重要的核心理念:一切皆为组件,Flutter 的所有元素都是由控件构成的。
与原生开发中控件所代表的含义不同,Flutter 中 widget 的概念更加广泛,它不仅可以表示 UI 元素,也可以表示一些功能性的组件,如用于手势检测的 GestureDetector widget、用于应用主题数据传递的 Theme 等等。而原生开发中的控件通常只是指 UI 元素。由于 Flutter 主要就是用于构建用户界面的,所以,在大多数时候,我们可以简单的认为 widget 就是一个控件,不必纠结于概念。

Widget 与 Element

在正式介绍 Flutter 的 Widget 之前,我们需要理清两个概念,即什么是 Widget,什么是 Element?

Widget 的功能是“描述一个 UI 元素的配置数据,它就是说,Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter 中真正代表屏幕上显示元素的类是 Element,也就是说 Widget 只是描述 Element 的一个配置。并且一个 Widget 可以对应多个 Element,这是因为同一个 Widget 对象可以被添加到 UI 树的不同部分,而真正渲染时,UI 树的每一个 Widget 节点都会对应一个 Element 对象。所以,理解 Flutter 的 Widget 需要理清两个概念:

  • Widget 实际上就是 Element 的配置数据, Widget 的功能是描述一个 UI 元素的一个配置数据, 而真正的 UI 渲染是由 Element 构成的。
  • 由于 Element 是通过 Widget 生成,所以它们之间有对应关系,所以在大多数场景,我们可以简单地认为 Widget 就是指 UI 控件或 UI 渲染。

Widget 声明

首先,我们先来看一下 Widget 类的声明:

@immutable
abstract class Widget extends DiagnosticableTree {const Widget({ this.key});
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {return key == null ? '$runtimeType' : '$runtimeType-$key';}

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

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

从这个 Widget 类的申明中,我们可以得到如下一些信息:

  • Widget 类继承自 DiagnosticableTree,主要作用是提供调试信息。
  • Key: 这个 key 属性类似于 React/Vue 中的 key,主要的作用是决定是否在下一次 build 时复用旧的 widget,决定的条件在 canUpdate()方法中
  • createElement():正如前文所述一个 Widget 可以对应多个 Element;Flutter Framework 在构建 UI 时,会先调用此方法生成对应节点的 Element 对象。此方法是 Flutter Framework 隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties 复写父类的方法,主要是设置 DiagnosticableTree 的一些特性。
  • canUpdate()是一个静态方法,它主要用于在 Widget 树重新 build 时复用旧的 widget。具体来说,是否使用新的 Widget 对象去更新旧 UI 树上所对应的 Element 对象的配置;并且通过其源码我们可以知道,只要 newWidget 与 oldWidget 的 runtimeType 和 key 同时相等时就会用 newWidget 去更新 Element 对象的配置,否则就会创建新的 Element。

StatelessWidget

StatelessWidget 是 Flutter 提供的一个不需要状态更改的 widget,它没有内部状态管理功能。StatelessWidget 相对比较简单,它继承自 Widget 类,重写了 createElement()方法。

@override
StatelessElement createElement() => new 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 的构造函数参数应使用命名参数,命名参数中的必要参数要添加 @required 标注,这样有利于静态代码分析器进行检查。另外,在继承 widget 时,第一个参数通常应该是 Key,另外,如果 Widget 需要接收子 Widget,那么 child 或 children 参数通常应被放在参数列表的最后。
然后,我们可以通过如下方式来使用 Echo widget。

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

运行后效果如下图所示:

StatefulWidget

StatefulWidget 是一个可变状态的 widget。使用 setState 方法管理 StatefulWidget 的状态的改变。调用 setState 告诉 Flutter 框架,某个状态发生了变化,Flutter 会重新运行 build 方法,以便应用程序可以应用最新状态。

和 StatelessWidget 一样,StatefulWidget 也是继承自 Widget 类,并重写了 createElement()方法,不同的是返回的 Element 对象并不相同;另外 StatefulWidget 类中添加了一个新的接口 createState()。

下面是 StatefulWidget 的类定义,如下所示:

abstract class StatefulWidget extends Widget {const StatefulWidget({ Key key}) : super(key: key);

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

  @protected
  State createState();}

StatefulElement 间接继承自 Element 类,它与 StatefulWidget 相对应(作为其配置数据)。同时,StatefulElement 中可能会多次调用 createState()来创建状态 (State) 对象。

createState() 用于创建和 Stateful widget 相关的状态,它在 Stateful widget 的生命周期中可能会被多次调用。例如,当一个 Stateful widget 同时插入到 widget 树的多个位置时,Flutter framework 就会调用该方法为每一个位置生成一个独立的 State 实例,其实,本质上就是一个 StatefulElement 对应一个 State 实例。

StatelessWidget 和 StatefulWidget 的区别

通过上面的讲解,我们可以得出如下结论:

  • StatelessWidget 是状态不可变的 widget, 初始状态设置以后就不可再变化, 如果需要变化需要重新创建 StatefulWidget,因为 StatefulWidget 可以保存自己的状态。
  • 在 Flutter 中通过引入 State 来保存状态, 当 State 的状态改变时,能重新构建本节点以及孩子的 Widget 树来进行 UI 变化。
  • 如果需要主动改变 State 的状态,需要通过 setState()方法进行触发,单纯改变数据是不会引发 UI 改变的

Widgets 的 State

说到组件,就不得不提到 Widgets 的 State。通常,一个 StatefulWidget 类会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息有如下两个作用:

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

State 有两个常用属性:widget 和 context。

  • widget:它表示与该 State 实例关联的 widget 实例,由 Flutter framework 动态设置。注意,这种关联并非永久的,因为在应用声明周期中,UI 树上的某一个节点的 widget 实例在重新构建时可能会变化,但 State 实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter framework 会动态设置 State.widget 为新的 widget 实例。
  • context,它是 BuildContext 类的一个实例,表示构建 widget 的上下文,它是操作 widget 在树中位置的一个句柄,它包含了一些查找、遍历当前 Widget 树的一些方法。每一个 widget 都有一个自己的 context 对象。

生命周期

和原生平台的控件一样,State 也有自己的生命周期。为了加深读者对 State 生命周期的印象,本节我们通过一个实例来演示一下 State 的生命周期。在接下来的示例中,我们实现一个计数器 widget,点击它可以使计数器加 1,由于要保存计数器的数值状态,所以我们应继承 StatefulWidget,代码如下:

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

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

CounterWidget 接收一个 initValue 整型参数,它表示计数器的初始值。接下来,我们看一下_CounterWidgetState 的实现:

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {super.initState();
    // 初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(child: Text('$_counter'),
          // 点击后计数器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  void deactivate() {super.deactivate();
    print("deactive");
  }

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

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

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

接下来,我们创建一个新路由,在新路由中,我们只显示一个 CounterWidget。

Widget build(BuildContext context) {return CounterWidget();
}

然后,运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字 0,并且控制台日志输出如下:

I/flutter (5436): initState
I/flutter (5436): didChangeDependencies
I/flutter (5436): build

可以看到,在 StatefulWidget 插入到 Widget 树时首先被调用的是 initState 方法。然后,我们点击⚡️按钮热重载代码,控制台输出日志如下:

I/flutter (5436): reassemble
I/flutter (5436): didUpdateWidget
I/flutter (5436): build

可以看到,热重载操作时 initState 和 didChangeDependencies 都没有被调用,而是调用了 didUpdateWidget。
接下来,我们在 widget 树中移除 CounterWidget,并将路由 build 方法改为:

Widget build(BuildContext context) {
  // 移除计数器 
  //return CounterWidget();
  // 随便返回一个 Text()
  return Text("xxx");
}

然后执行热重载操作,日志如下:

I/flutter (5436): reassemble
I/flutter (5436): deactive
I/flutter (5436): dispose

可以看到,在 CounterWidget 从 widget 树中移除时,deactive 和 dispose 会依次被调用。

通过上面的示例,我们将 StatefulWidget 生命周期整理如下图:

StatefulWidget 的生命周期大致可分为三个阶段:

  • 初始化:插入渲染树,这一阶段涉及的生命周期函数主要有 createState、initState、didChangeDependencies 和 build。
  • 运行中:在渲染树中存在,这一阶段涉及的生命周期函数主要有 didUpdateWidget 和 build。
  • 销毁:从渲染树中移除,此阶段涉及的生命周期函数主要有 deactivate 和 dispose。

初始化阶段

createState:createState 必须且仅执行一次,它用来创建 state,当创建 StatefulWidget 时,该放方法就会被执行。

initState:在创建 StatefulWidget 后,initState 是第一个被调用的方法,同 createState 一样只被调用一次,此时 widget 的被添加至渲染树,mount 的值会变为 true,但并没有渲染。我们可以在该方法内做一些初始化操作。

didChangeDependencies:当 widget 第一次被创建时,didChangeDependencies 紧跟着 initState 函数之后调用,在 widget 刷新时,该方法不会被调用。它会在“依赖”发生变化时被 Flutter Framework 调用,这个依赖是指 widget 是否使用父 widget 中 InheritedWidget 的数据。也即是只有在 widget 依赖的 InheritedWidget 发生变化之后,didChangeDependencies 才会调用。
这种机制可以使子组件在所依赖的 InheritedWidget 变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子 widget 的 didChangeDependencies 方法将会被调用。

build:build 会在 widget 第一次创建时紧跟着 didChangeDependencies 方法之后和 UI 重新渲染时被调用。build 只做 widget 的创建操作,如果在 build 里做其他操作,会影响 UI 的渲染效果。

运行中

StatefulWidget 运行中只会调用两个函数,即 didUpdateWidget 和 build。
didUpdateWidget:当组件的状态改变的时候就会调用 didUpdateWidget, 比如调用了 setState。

销毁

deactivate:当 State 对象从树中被移除时,会调用此回调函数,这标志着 StatefulWidget 将要执行销毁操作。页面切换时,也会调用它,因为此时 State 在视图树中的位置发生了变化但是 State 不会被销毁,而是重新插入到渲染树中。重写的时候必须要调用 super.deactivate()

dispose:从渲染树中移除时调用,State 会永久的从渲染树中移除,和 initState 正好相反 mount 值变味 false。这时候就可以在 dispose 里做一些取消监听操作。

为了方便读者理解,我们看一下 StatefulWidget 的生命周期函数调用情况。

生命周期 调用次数 调用时间
createState 1 组件创建时
initState 1 组件创建时
didChangeDependencies n 组件创建或状态发生变化
build n 组件创建或 UI 重新渲染
didUpdateWidget n 组件创建或 UI 重新渲染
deactivate n State 对象将要移除时
dispose 1 state 对象被销毁

内置组件库

Flutter SDK 提供了一套丰富、强大的基础组件,在基础组件库之上 Flutter 又提供了一套 Material 风格(Android 默认的视觉风格)和一套 Cupertino 风格(iOS 视觉风格)的组件库。使用前只需要导入即可使用:

import 'package:flutter/widgets.dart';

基础组件

Flutter SDK 提供了很多功能丰富的基础组件,常见的有如下一些:

  • Text:该组件可让您创建一个带格式的文本。
  • Row、Column:这些具有弹性空间的布局类 Widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。
  • Stack:取代线性布局 (译者语:和 Android 中的 FrameLayout 相似),Stack 允许子 widget 堆叠,你可以使用 Positioned 来定位他们相对于 Stack 的上下左右四条边的位置。Stacks 是基于 Web 开发中的绝对定位(absolute positioning )布局模型设计的。
  • Container:Container 可让您创建矩形视觉元素。container 可以装饰一个 BoxDecoration, 如 background、一个边框、或者一个阴影。Container 也可以具有边距(margins)、填充 (padding) 和应用于其大小的约束(constraints)。另外,Container 可以使用矩阵在三维空间中对其进行变换。

Material 组件

众所周知,Material 是 Android 应用默认的视觉风格,Cupertino 则是 iOS 应用的默认视觉风格,为了实现两种不同的视觉风格,Flutter 在基础组件库之上 Flutter 又提供了一套 Material 风格和一套 Cupertino 风格的组件库,以满足两种不同设计风格的开发需要。

Material 应用程序以 MaterialApp 组件开始,该组件在应用程序的根部创建了一些必要的组件,比如 Theme 组件,它用于配置应用的主题。是否使用 MaterialApp 完全是可选的,但是使用它是一个很好的做法。在之前的示例中,我们已经使用过多个 Material 组件了,如:Scaffold、AppBar、FlatButton 等。

要使用 Material 组件,需要先引入它:

import 'package:flutter/material.dart';

Cupertino 组件

Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富,但是它仍在不断的完善中。目前,Flutter 提供的 Cupertino 组件主要有 CupertinoTabBar、CupertinoActivityIndicator、CupertinoPageScaffold、CupertinoTabScaffold、CupertinoTabView 等。
关于 Cupertino 组件,大家可以参考官方的介绍:Cupertino (iOS 风格) Widgets

退出移动版