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中的保存的状态信息有如下两个作用:
- 在widget build时可以被同步读取。
- 在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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。