阅读 28

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

关注下面的标签,发现更多相似文章
评论