- 原文地址:Widget - State - Context - InheritedWidget
- 原文做者:www.didierboelens.com
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:nanjingboy
- 校对者:Mirosalva、HearFishle
本文涵盖了 Flutter 应用中有关 Widget、State、Context 及 InheritedWidget 的重要概念。由于 InheritedWidget 是最重要且文档缺少的部件之一,故需特别关注。html
难度:初学者前端
Flutter 中的 Widget、State 及 Context 是每一个 Flutter 开发者都须要充分理解的最重要的概念之一。react
虽然存在大量文档,但并无一个可以清晰地解释它。android
我将用本身的语言来解释这些概念,知道这些可能会让一些纯理论者感到不安,但本文的真正目的是试图说清如下主题:ios
本文同时发布于 Medium - Flutter Community。git
在 Flutter 中,几乎全部的东西都是 Widget。github
将一个 Widget 想象为一个可视化组件(或与应用可视化方面交互的组件)。后端
当你须要构建与布局直接或间接相关的任何内容时,你正在使用 Widget。服务器
Widget 以树结构进行组织。markdown
包含其余 Widget 的 Widget 被称为父 Widget(或Widget 容器)。包含在父 Widget 中的 Widget 被称为子 Widget。
让咱们用 Flutter 自动生成的基础应用来讲明这一点。如下是简化代码,仅有 build 方法:
@override
Widget build(BuildContext){
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
复制代码
若是咱们如今观察这个基本示例,咱们将得到如下 Widget 树结构(限制代码中存在的 Widget 列表):
另一个重要的概念是 Context。
Context 仅仅是已建立的全部 Widget 树结构中某个 Widget 的位置引用。
简而言之,将 context 做为 Widget 树的一部分,其中 context 所对应的 Widget 被添加到此树中。
一个 context 仅仅从属于一个 widget。
若是 widget ‘A’ 拥有子 widget,那么 widget ‘A’ 的 context 将成为其直接关联子 context 的父 context。
读到这里会很明显发现 context 是连接在一块儿的,而且会造成一个 context 树(父子关系)。
若是咱们如今尝试在上图中说明 Context 的概念,咱们获得(依旧是一个很是简化的视图)每种颜色表明一个 context(除了 MyApp,它是不一样的):
Context 可见性 (简短描述):
某些东西 只能在本身的 context 或在其父 context 中可见。
经过上述描述咱们能够将其从子 context 中提取出来,它很容易找到一个 祖先(= 父)Widget。
一个例子,考虑 Scaffold > Center > Column > Text:context.ancestorWidgetOfExactType(Scaffold) => 经过从 Text 的 context 获得树结构来返回第一个 Scaffold。
从父 context 中,也能够找到 后代(= 子)Widget,但不建议这样作(咱们将稍后讨论)。
Widget 拥有 2 种类型:
这些可视化组件除了它们自身的配置信息外不依赖于任何其余信息,该信息在其直接父节点构建时提供。
换句话说,这些 Widget 一旦建立就不关心任何变化。
这样的 Widget 称为 Stateless Widget。
这种 Widget 的典型示例能够是 Text、Row、Column 和 Container 等。在构建时,咱们只需将一些参数传递给它们。
参数能够是装饰、尺寸、甚至其余 widget 中的任何内容。须要强调的是,该配置一旦被建立,在下次构建过程以前都不会改变。
stateless widget 只有在 loaded/built 时才会绘制一次,这意味着任何事件或用户操做都没法对该 Widget 进行重绘。
如下是与 Stateless Widget 相关的典型代码结构。
以下所示,咱们能够将一些额外的参数传递给它的构造函数。但请记住,这些参数在后续阶段将不改变(变化),而且必须按照已有状态使用。
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context){
return new ...
}
}
复制代码
即便有另外一个方法能够被重写(createElement),后者也几乎不会被重写。惟一须要被重写的是 build 方法。
这种 Stateless Widget 的生命周期是至关简单的:
其余一些 Widget 将处理一些在 Widget 生命周期内会发生变化的内部数据。所以,此类数据会变为动态。
该 Widget 所持有的数据集在其生命周期内可能会发生变化,这样的数据被称为 State。
这些 Widget 被称为 Stateful Widget。
此类 Widget 的示例多是用户可选择的复选框列表,也能够是根据条件禁用的 Button 按钮。
State 定义了 StatefulWidget 实例的 “行为”。
它包含了用于 交互 / 干预 Widget 信息:
应用于 State 的任何更改都会强制 Widget 进行重建。
对于 Stateful Widget,State 与 Context 相关联。而且此关联是永久性的,State 对象将永远不会改变其 context。
即便能够在树结构周围移动 Widget Context,State 仍将与该 context 相关联。
当 State 与 Context 关联时,State 被视为已挂载。
重点:
State 对象 与 context 相关联,就意味着该 State 对象是不(直接)访问另外一个 context!(咱们将在稍后讨论该问题)。
既然已经介绍了基本概念,如今是时候更加深刻一点了……
如下是与 Stateful Widget 相关的典型代码结构。
因为本文的主要目的是用“变量”数据来解释 State 的概念,我将故意跳过任何与 Stateful Widget 相关的一些可重写方法的解释,这些方法与此没有特别的关系。这些可重写的方法是 didUpdateWidget、deactivate 和 reassemble。这些内容将在下一篇文章中讨论。
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.parameter,
}): super(key: key);
final parameter;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState(){
super.initState();
// Additional initialization of the State
}
@override
void didChangeDependencies(){
super.didChangeDependencies();
// Additional code
}
@override
void dispose(){
// Additional disposal code
super.dispose();
}
@override
Widget build(BuildContext context){
return new ...
}
}
复制代码
下图展现了与建立 Stateful Widget 相关的操做/调用序列(简化版本)。在图的右侧,你将注意到数据流中 State 对象的内部状态。你还将看到此时 context 与 state 已经关联,而且 context 所以变为可用状态(mounted)。
接下来让咱们经过一些额外的细节来解释它:
一旦 State 对象被建立,initState() 方法是第一个(构造函数以后)被调用的方法。当你须要执行额外的初始化时,该方法将会被重写。常见的初始化与动画、控制器等相关。若是重写该方法,你应该首先调用 super.initState()。
该方法能够获得 context,但没法真正使用它,由于框架尚未彻底将其与 state 关联。
一旦 initState() 方法执行完成,State 对象就被初始化而且 context 变为可用。
在该 State 对象的生命周期内将不会再次调用此方法。
didChangeDependencies() 方法是第二个被调用的方法。
在这一阶段,因为 context 是可用的,因此你可使用它。
若是你的 Widget 连接到了一个 InheritedWidget 而且/或者你须要初始化一些 listeners(基于 context),一般会重写该方法。
请注意,若是你的 widget 连接到了一个 InheritedWidget,在每次重建该 Widget 时都会调用该方法。
若是你重写该方法,你应该首先调用 super.didChangeDependencies()。
build(BuildContext context) 方法在 didChangeDependencies()(及 didUpdateWidget)以后被调用。
这是你构建你的 widget(可能还有任何子树)的地方。
每次 State 对象更新(或当 InheritedWidget 须要通知“已注册” widget)时都会调用该方法!!
为了强制重建,你可能须要调用 setState((){…}) 方法。
dispose() 方法在 widget 被废弃时被调用。
若是你须要执行一些清理操做(好比:listeners),则重写该方法,并在此以后当即调用 super.dispose()。
这是许多开发者都须要问本身的问题:我是否须要 Widget 为 Stateless 或 Stateful?
为了回答这个问题,请问问本身:
在个人 widget 生命周期中,是否须要考虑一个将要变动,而且在变动后 widget 将强制重建的变量?
若是问题的答案是 yes,那么你须要一个 Stateful Widget,不然,你须要一个 Stateless Widget。
一些例子:
用于显示复选框列表的 widget。要显示复选框,你须要考虑一系列项目。每一个项目都是一个包含标题和状态的对象。若是你点击一个复选框,相应的 item.status 将会切换;
在这种状况下,你须要使用一个 Stateful Widget 来记住项目的状态,以便可以重绘复选框。
带有表格的屏幕。该屏幕容许用户填写表单的 Widget 并将表单发送到服务器。
在这种状况下,除非你要对表单进行验证,或在提交以前作一些其余的事情,一个 Stateless Widget 可能就足够了。
还记得 Stateful widget 的结构吗?有 2 个部分:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
复制代码
第一部分 “MyStatefulWidget” 一般是 Widget 的 public 部分。当你须要将其添加到 widget 树时,能够实例化它。该部分在 Widget 生命周期内不会发生变化,但可能接受与其相关的 State 实例化时使用的参数。
请注意,在 Widget 第一部分定义的任何变量一般在其生命周期内不会发生变化。
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
复制代码
第二部分 “_MyStatefulWidgetStat” 管理 Widget 生命周期中的变化,并强制每次应用修改时重建该 Widget 实例。名称开头的 ‘_’ 字符使得该类对 .dart 文件是私有的。
若是你须要在 .dart 文件以外引用此类,请不要使用 ‘_’ 前缀。
_MyStatefulWidgetState
类能够经过使用 widget.{变量名称} 来访问被存储在 MyStatefulWidget 中的任何变量。在该示例中为:widget.color。
在 Fultter 中,每个 Widget 都是被惟一标识的。这个惟一标识在 build/rendering 阶段由框架定义。
该惟一标识对应于可选的 Key 参数。若是省略该参数,Flutter 将会为你生成一个。
在某些状况下,你可能须要强制使用此 key,以即可以经过其 key 访问 widget。
为此,你可使用如下方法中的任何一个:GlobalKey、LocalKey、UniqueKey 或 ObjectKey。
GlobalKey 确保生成的 key 在整个应用中是惟一的。
强制 Widget 使用惟一标识:
GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
复制代码
如前所述,State 被连接到 一个 Context,而且一个 Context 被连接到一个 Widget 实例。
从理论上讲,惟一可以访问 State 的是 Widget State 自身。
在此中状况下不存在任何困难。Widget State 类能够访问任何内部变量。
有时,父 widget 可能须要访问其直接子节点的 State 才能执行特定任务。
在这种状况下,要访问这些直接子节点的 State,你须要了解它们。
呼叫某人的最简单方法是经过名字。在 Flutter 中,每一个 Widget 都有一个惟一的标识,由框架在 build/rendering 时肯定。如前所示,你可使用 key 参数为 Widget 强制指定一个标识。
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = new GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return new MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
复制代码
一经肯定,父 Widget 能够经过如下形式访问其子节点的 State:
myWidgetStateKey.currentState
让咱们考虑当用户点击按钮时显示 SnackBar 这样一个基本示例。因为 SnackBar 是 Scaffold 的子 Widget,它不能被 Scaffold 内部任何其余子节点直接访问(还记得 context 的概念以及其层次/树结构吗?)。所以,访问它的惟一方法是经过 ScaffoldState,它暴露出一个公共方法来显示 SnackBar。
class _MyScreenState extends State<MyScreen> {
/// the unique identity of the Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context){
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('My Screen'),
),
body: new Center(
new RaiseButton(
child: new Text('Hit me'),
onPressed: (){
_scaffoldKey.currentState.showSnackBar(
new SnackBar(
content: new Text('This is the Snackbar...'),
)
);
}
),
),
);
}
}
复制代码
假设你有一个属于另外一个 Widget 的子树的 Widget,以下图所示。
为了实现这一目标,须要知足 3 个条件:
为了暴露 其 State,Widget 须要在建立时记录它,以下所示:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
return myState;
}
}
复制代码
为了让“其余类” 设置/获取 State 中的属性,Widget State 须要经过如下方式受权访问:
例子:
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
复制代码
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
final MyExposingWidgetState state = widget?.myState;
return new Container(
color: state == null ? Colors.blue : state.color,
);
}
}
复制代码
这个解决方案很容易实现,但子 widget 如何知道它什么时候须要重建呢?
经过此方案,它无能为力。它必须等到重建发生后才能刷新其内容,此方法不是特别方便。
下一节将讨论 Inherited Widget 的概念,它能够解决这个问题。
简而言之,InheritedWidget 容许在 widget 树中有效地向下传播(和共享)信息。
InheritedWidget 是一个特殊的 Widget,它将做为另外一个子树的父节点放置在 Widget 树中。该子树的全部 widget 都必须可以与该 InheritedWidget 暴露的数据进行交互。
为了解释它,让咱们思考如下代码片断:
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 提供某些『共享』数据。
如前所述,为了可以传播/共享某些数据,须要将 InheritedWidget 放置在 widget 树的顶部,这解释了传递给 InheritedWidget 基础构造函数的 @required Widget child 参数。
static MyInheritedWidget of(BuildContext context) 方法容许全部子 widget 经过包含的 context 得到最近的 MyInheritedWidget 实例(参见后面的内容)。
最后重写 updateShouldNotify 方法用来告诉 InheritedWidget 若是对数据进行了修改,是否必须将通知传递给全部子 widget(已注册/已订阅)(请参考下文)。
所以,咱们须要将它放在树节点级别,以下所示:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: new Row(
children: <Widget>[
...
],
),
);
}
}
复制代码
在构建子节点时,后者将得到 InheritedWidget 的引用,以下所示:
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
///
/// 此刻,该 widget 可以使用 MyInheritedWidget 暴露的数据
/// 经过调用:inheritedWidget.data
///
return new Container(
color: inheritedWidget.data.color,
);
}
}
复制代码
请思考下图中所显示的 widget 树结构。
为了说明交互方式,咱们作如下假设:
针对该场景,InheritedWidget 是惟一一个合适的 Widget 选项!
咱们先写下代码而后再进行解释:
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() => new 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(new Item(reference));
});
}
@override
Widget build(BuildContext context){
return new _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
new WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C');
}
}
复制代码
在这个很是基本的例子中:
_MyInherited
是一个 InheritedWidget,每次咱们经过 ‘Widget A’ 按钮添加一个项目时它都会从新建立这一切是如何运做的呢?
当一个子 Widget 调用 MyInheritedWidget.of(context) 时,它传递自身的 context 并调用 MyInheritedWidget 的如下方法。
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
复制代码
在内部,除了简单地返回 MyInheritedWidgetState 实例外,它还订阅消费者 widget 以便用于通知更改。
在幕后,对这个静态方法的简单调用实际上作了 2 件事:
_MyInherited
)应用修改时,该 widget 可以重建_MyInherited
widget(又名 MyInheritedWidgetState)中引用的数据将返回给消费者因为 ‘Widget A’ 和 ‘Widget B’ 都使用 InheritedWidget 进行了订阅,所以若是对 _MyInherited
应用了修改,那么当点击 Widget A 的 RaisedButton 时,操做流程以下(简化版本):
_MyInherited
新的实例_MyInherited
记录经过参数(data)传递的新 State至此它可以有效工做!
然而,Widget A 和 Widget B 都被重建了,但因为 Wiget A 没有任何改变,所以它没有重建的必要。那么应该如何防止此种状况发生呢?
Widget A 同时被重建的缘由是因为它访问 MyInheritedWidgetState 的方式。
如前所述,调用 context.inheritFromWidgetOfExactType() 方法实际上会自动将 Widget 订阅到消费者列表中。
避免自动订阅,同时仍然容许 Widget A 访问 MyInheritedWidgetState 的解决方案是经过如下方式改造 MyInheritedWidget 的静态方法:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
复制代码
经过添加一个 boolean 类型的额外参数……
所以,要完成此方案,咱们还须要稍微修改一下 Widget A 的代码,以下所示(咱们添加值为 false 的额外参数):
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');
},
),
);
}
}
复制代码
就是这样,当咱们按下 Widget A 时,它不会再重建了。
Routes、Dialogs 的 context 与 Application 绑定。
这意味着即便在屏幕 A 内部你要求显示另外一个屏幕 B(例如,在当前的屏幕上),也没法轻松地从两个屏幕中的任何一个关联它们本身的 context。
屏幕 B 想要了解屏幕 A 的 context 的惟一方法是经过屏幕 A 获得它并将其做为参数传递给 Navigator.of(context).push(….)
关于这些主题还有不少话要说……,特别是在 InheritedWidget 上。
在下一篇文章中我将介绍 Notifiers / Listeners 的概念,它们使用 State 和数据传递的方式上一样很是有趣。
因此,请保持关注和快乐编码。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。