1.前言
本文涵盖了Widget,State,BuildContext,InheritedWidget等术语的相关概念,并着力解答以下几个问题:
- StatefulWidget与StatelessWidget的区别;
- 什么是BuildContext;
- 什么是State,我们该如何运用;
- BuildContext与State之间的关系;
- InheritedWidget以及Widget树间的信息传递;
- rebuild概念。
2.Widget
在Flutter中,几乎任何事物都是Widget。
可以把Widget想象成一种可视化组件,或者应用中可以与可视化组件进行交互的模块。
Widget树
Flutter中所有的WIdget都以树状结构呈现。
一个包含其他Widget的Widget被称为父Widget或者Widget容器,被包含在父Widget下的Widget被称为子Widget。
下面我们来分析示例代码中的Widget树结构:
其树状结构如下:
widget treeBuildContext
BuildContext是Widget树结构中每个Widget的上下文环境,每个BuildContext都只属于一个Widget。
如果Widget A有多个子Widget,则Widget A的BuildContext是其子Widget的BuildContext的父context。
为了便于理解每个context的作用范围,我们将前文中的Widget树状图中的BuildContext用颜色进行标注,效果如下:
从BuildContext的继承关系中,我们可以很容易找到Widget的父级(祖先)Widget。例如,在上图Scaffold > Center > Column > Text这一结构中,
通过代码context.ancestorWidgetOfExactType(Scaffold)
就可以获取到当前context下的第一个Scaffold。
同理,也可以通过父context的关系找到对应的子Widget,但是并不推荐这么使用,我们将在后文解释原因。
StatelessWidget
StatelessWidget一旦创建就无法进行修改,这意味着它不会因为外部条件变化而重新绘制。
一个典型的StatelessWidget示例如下:
如代码所见,StatelessWidget的生命周期非常简单明了:
- 初始化;
- 通过
build()
方法进行渲染。
StatefulWidget
与StatelessWidget相对应的另一种Widget,它可以在其生命周期中操作内部持有数据的变化,这些数据被称为State,这样的Widget也叫做StatefulWidget。
典型的StatefulWidget有Checkbox,Radio,Switch等相关组件,其State发生的变化将直接体现到UI上进行更新。
简单的StatefulWidget示例:
我们将会在State部分详细讲解StatefulWidget的结构与生命周期。
3.State
State作为StatefulWidget内部数据,它的作用主要在于两点:
- 定义Widget的交互行为;
- 调整Widget的布局显示。
任何State一旦发生调整都会使StatefulWidget进行rebuild操作。
BuildContext与State关系
在StatefulWidget中,State与BuildContext唯一相关,State不能修改所属的BuildContext,而且当Widget在树结构中发生位置变化时(该操作也会导致BuildContext的变化),这样的关系依然保持。
可以这样认为,一旦State与BuildContext建立了关联,这种关系将一直固定存在,意味着我们不能直接通过其他BuildContext获取到当前context下的state。
StatefulWidget生命周期
正如前文示例代码所示,State作为StatefulWidget的主体,它可以在多个节点(@override所标记的重写方法)对State进行调整。
当然,还有didUpdateWidget()
,deactivate()
,reassemble()
等重写方法并不在本文范畴中。
在下面的时序图中我们将完整地了解StatefulWidget各个方法的调用顺序(已省略部分方法),以及State与BuildContext的关联时机,State的生效时机等:
initState()
initState()
是构造方法执行之后第一个调用的方法,它的执行完成标志着state对象初始化完毕,并且在生命周期中只被调用一次。
该方法重写主要完成一些额外的初始化工作,例如animation和controller的相关初始化等,重写时需要调用super.initState()
来完成父类的初始化。
在该方法中,context对象可以访问但是并不能拿来使用,因为此时state与context并没有建立关联。
didChangeDependencies()
didChangeDependencies()
是第二个调用的方法,在这一步中context可以直接使用。
该方法一般在Widget自身和InheritedWidget相关联时或者需要创建基于context的监听时需要重写,且重写时需要调用super.didChangeDependencies()
。
注意,如果Widget和InheritedWidget进行了关联,则Widget每一次进行rebuild操作时该方法都会重复调用。
build()
build()
方法在didChangeDependencies()
和didUpdateWidget()
之后执行,是构建Widget及其树形结构的位置。
该方法会在state对象发生改变或者InheritedWidget向其注册的Widget发起通知时进行调用,我们可以在setState((){...})
方法的闭包中强制Widget进行rebuild(重绘)操作。
dispose()
dispose()
方法会在Widget销毁时调用。该方法进行一些清理操作(例如,listener,controller等),注意在方法最后调用super.dispose()
。
如何选择StatefulWidget与StatelessWidget
回答这个问题之前,我们先不妨问问自己:在Widget是生命周期中,是否需要一个变量来改变Widget,并且考虑如何对Widget进行rebuild操作。
如果我们回答yes,那就需要StatefulWidget,否则,就应该选择StatelessWidget。
举个两个栗子:
- 试想需要创建一个包含CheckBox的列表,列表中的每一项都包含了标题和CheckBox的状态。当点击列表中的每一项时,CheckBox的状态也随之切换。在这种场景下,需要使用StatefulWidget来记录每一项的状态,以及通过它才能对CheckBox进行重绘。
- 在界面中有一个Form表单,表单允许用户输入数据并发送到服务器。如果不需要在提交前做一些数据验证或者其他操作,StatelessWidget足够可用。
StatefulWidget结构
正如前文代码段所示,StatefulWidget包含两个部分:
- 定义Widget部分;
- 定义State部分。
定义Widget部分
该部分属于StatefulWidget的public部分(文件外部可以通过import
访问到),在这里可以对Widget做一些初始化自定义,以及通过重写createState()
方法与私有的State对象进行关联。
注意,任何需要调整的变量都不应该定义在这里,因为在整个Widget生命周期中这里的变量都不会被改变,示例代码中
parameter
前的final
关键字也说明了这点。
定义State部分
该部分属于StatefulWidget的private部分(dart语言中以下划线开头声明的类名,方法名,变量名等都属于作用域范围下的私有声明),这里定义的变量在Widget生命周期中可以调整,并且这些调整可以应用到Widget的重绘上。
同时,_MyStatefulWidgetState
内部可以通过widget.{变量名}
访问到与之关联的Widget中的变量,例如widget.parameter。
Widget唯一标识 - Key
在Flutter中,每个Widget都有唯一标识(Key),该标识由系统框架创建,并且传递给Widget构造方法中的可选参数*key*
。
如果不显式地传入key,系统会自动创建一个,在一些特殊情况下,必须传入key值,例如需要通过key来直接获取对应Widget的时候。
Flutter框架提供了下列key方案:
GlobalKey
该key保证整个App内部都是唯一的,但是创建GlobalKey的代价非常昂贵,如果不需要保证整个App内部唯一性,可以考虑使用LocalKey。
LocalKey
与GlobalKey相对应的一种局部key,需要保证创建LocalKey的Widget都有同一个父Widget,不然就失去其作用。
UniqueKey
UniqueKey必须保证关联的Widget只有一个child,属于LocalKey。
ObjectKey
对象级别的key,通过Widget的实例对象来创建,与之类似的还有ValueKey,它使用了类型来作为key值,均属于LocalKey。
访问State
正如前文所述,State与BuildContext相关联,而BuildContext又与Widget相关联。
Widget自身
在理论上是唯一能够访问state的对象,而且state也可以直接访问Widget的变/常量。
子Widget
某些时候父类Widget可能会需要访问子类Widget的state中的值来完成一些特殊操作。
为了满足这一需求,最简单的就是通过key来获取。针对以下代码,我们可以使用myWidgetStateKey.currentState
来获取state值:
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
注意,MyStatefulWidgetState类名没有下划线前缀,因为我们需要将其暴露出来才可以访问到。
父Widget
试想有以下Widget的树形结构,底层的子Widget想访问根节点Widget的state:
想要达成这一目标需要满足3个条件:
- 根节点Widget需要暴露state变量,不再将state声明为私有类型;
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = MyExposingWidgetState();
return myState;
}
}
- State必须为其中的值创建getter/setter或者声明值为
public
(不推荐);
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
- 底层Widget获取到state的引用。
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
final MyExposingWidgetState state = widget?.myState;
return Container(
color: state == null ? Colors.blue : state.color,
);
}
}
虽然上述方案可以解决在任何地方访问state的问题,但并不能感知state内部的值何时进行修改,随之而来的Widget何时进行重绘等问题,InheritedWidget就能帮助我们解决这一问题。
4.InheritedWidget
简而言之,InheritedWidget可以帮助我们在Widget树形结构中高效地传递数据信息。作为一种特殊的Widget,它可以使Widget树中所有的Widget都能够共享数据。
基本概念
为了更加清楚的解释相关概念,我们以下面代码进行说明:
class MyInheritedWidget extends InheritedWidget {
MyInheritedWidget({
Key key,
@required Widget child,
this.data,
}): super(key: key, child: child);
final data;
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}
代码中定义了名为MyInheritedWidget
的Widget,它的目的即在其Widget子树中共享其data变量。为了实现这一目的,我们还需要为它传入子Widget作为构造方法的参数,才使得其子树间的共享数据成为可能。换个更简单的说法,如果想要某个Widget的子节点能共享数据,请使用InheritedWidget来"包裹"它。
再来看看静态方法static MyInheritedWidget of(BuildContext context)
,则实现了从BuildContext中获取具体类型Widget的功能。
最后,重写updateShouldNotify
方法来告知InheritedWidget的子Widget(订阅/注册过数据的修改通知)是否需要接收更新。
使用InheritedWidget时,只需编写类似如下代码:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return MyInheritedWidget(
data: counter,
child: Row(
children: <Widget>[
...
],
),
);
}
}
子Widget访问数据
子Widget可以通过获得InheritedWidget引用来访问数据:
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
///
/// From this moment, the widget can use the data, exposed by the MyInheritedWidget
/// by calling: inheritedWidget.data
///
return Container(
color: inheritedWidget.data.color,
);
}
}
Widget交互
试想有如下WIdget树结构:
cart sample of widget tree为了举例说明图中结构,我们假设以下场景:
- Widget A是将商品添加到购物车的按钮;
- Widget B是展示购物车中商品数量的文本;
- Widget C是WIdget B同级的其他文本;
- 我们希望按下按钮(Widget A)时 Widget B能够准确显示购物车中的商品数量,而Widget C并不会发生任何重绘。
为了模拟这一需求,我们编写以下代码:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
}
class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
}
class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add( Item(reference));
});
}
@override
Widget build(BuildContext context){
return _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return MyInheritedWidget(
child: Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: Column(
children: <Widget>[
WidgetA(),
Container(
child: Row(
children: <Widget>[
Icon(Icons.shopping_cart),
WidgetB(),
WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return Container(
child: RaisedButton(
child: Text('Add Item'),
onPressed: () {
state.addItem(' item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am Widget C');
}
}
简要说明一下每个类的功能:
-
_MyInherited
是一个InheritedWidget,在点击Widget A时会重复创建。 -
MyInheritedWidget
是一个StatefulWidget,其state管理着一个商品数组,通过静态方法static MyInheritedWidgetState of(BuildContext context)
来获取state对象。 -
MyInheritedWidgetState
是管理商品数组的state类,同时创建一个itemsCount
的getter方法以及addItem(String preference)
方便外部调用。State中每加入一个商品,build(BuildContext context)
方法会创建一个_MyInherited
对象。 -
MyTree
创建了结构图中的Widget树结构,并以MyInheritedWidget
为根节点。 -
WidgetA
是一个RaiseButton类型的Widget,点击之后调用state的addItem(String preference)
方法以完成商品的添加操作。 -
WidgetB
是一个简单的文本,用于展示购物车中的商品数量。
那么,代码是如何实现Widget向State进行注册的呢?关键点就在于静态方法static MyInheritedWidgetState of(BuildContext context)
的内部实现:
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
当子Widget调用该方法时,会传递其BuildContext,并返回对应MyInheritedWidgetState
类型的实例对象。如此一来,该方法完成了两个目的:
- 作为数据消费者的Widget被添加到订阅者名单中,当InheritedWidget管理的state发生数据变更时,它会接收到通知以准备重绘操作;
- 消费者Widget会同时获得数据管理者state的引用。
数据流向
由于WidgetA
(RaiseButton)和WidgetB
(文本)均通过InheritedWidget订阅更改,所以任何传递到_MyInherited
更新的数据流向可以由以下(简化)流程表示:
- 点击按钮之后,调用
MyInheritedWidgetState
的addItem
方法; -
addItem
方法添加一个新的Item到_items
中; -
setState()
闭包调用以准备MyInheritedWidget
的rebuild; - 执行
build()
方法后,包含data(_items
)的_MyInherited
对象被创建; -
_MyInherited
通过构造方法记录新的state; -
_MyInherited
设置updateShouldNotify
回调为true以完成对订阅者的通知; -
_MyInherited
遍历所有订阅者(包括WidgetA
和WidgetB
),通知他们进行rebuild; -
WidgetC
不是订阅者,因此不会rebuild。
然而,这样一来,WidgetA
和WidgetB
都会进行rebuild,但是WidgetA
自身并不需要rebuild,那如何防止访问到InheritedWidget的部分Widget不rebuild呢?
其实,之所以会出现这样的情况,原因在于context.inheritFromWidgetOfExactType()
方法会自动将Widget作为订阅链表上的一员,要防止这种情况发生需修改为如下代码:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]) {
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
然后修改WidgetA
如下:
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
网友评论