Flutter 深入理解 State
本文主要介绍 Flutter 应用程序中 Widget,State,Context 和 InheritedWidget 等重要概念。 尤其注意的是 InheritedWidget,它是最重要且介绍较少的 Widget 之一。
前言
每个 Flutter 开发人员都需要完全理解 Widget,State 和 Context 概念。
虽然,有很多文档可以查询,但想要清晰地解释这些概念,还是有一定难度。
本文会对这些概念进行剖析,力图使你能够进一步了解这些概念:
- 有状态和无状态
Widegt的区别 Context是什么State是什么以及如何使用它Context与其State对象之间的关系InheritedWidget以及在Widgets树中传递信息的方式rebuild概念
第一部分:概念
Widget
在 Flutter 中,一切都是 Widget。
将 Widget 视为可视化组件(或可视化交互的组件)。
当您需要构建直接或间接布局关系时,您正在使用 Widget。
Widget 树
Widget 是以树结构组织起来的。
包含其他 Widget 的 Widget 称为父 Widget(或 Widget 容器)。包含在父 Widget 中的 Widget 称为子 Widget。
以 Flutter 自动生成的应用程序来进行说明。 构建代码:
1 | @override |
我们将获得以下 Widget 树(仅限代码中存在的 Widget 列表):
Context 上下文
另一个重要的概念是 Context。
Context 是构建所有 Widgets 树结构中的 Widget 的位置引用。
简而言之,将
Context视为Widgets树的一部分。
一个 Context 仅属于一个 Widget。同样具有父子关系,如果 Widget A 具有子 Widgets,则 Widget A 的 Context 将成为子 Widget 的 Context 的父 Context。
很明显,Context 是关联在一起,组成一个 Context 树(父子关系)。
现在,我们使用上图说明 Context 的概念,每种颜色代表一个 Context(除了 MyApp,它是不同的):
Context可见性:某些属性只能在其自己的Context中或在其父Context中可见。
使用这种语法,我们可以从子 Context 追根溯源,很容易找到一个祖先(或者父)Widget。
举例,考虑
Scaffold > Center > Column > Text:context.ancestorWidgetOfExactType(Scaffold)=>通过从Text Context向上查找到顶部Scaffold。
从父 Context 中,也可以找到后代(=子) Widget,但不建议这样做(我们稍后会讨论)。
两种 Widgets
无状态 StatelessWidget
这些可视组件,只依赖它们自己的配置信息,并不依赖于任何外部信息。这些配置信息在构建时由其父组件提供。
换句话说,这些 Widgets 一旦创建就不关心任何变化。
这些 Widgets 被称为无状态 Widgets。
这些 Widgets 的典型示例是 Text,Row,Column,Container ...... 在构建时,我们只是将一些参数传递给它们。
参数可以是文本,尺寸甚至其他 Widgets。 唯一重要的是这个配置信息一旦应用,在下一个构建过程之前不会改变。
无状态
Widget只能在加载或者构建Widget时绘制一次,这意味着无法基于任何事件或用户操作重绘该Widget。
StatelessWidget 生命周期
无状态 Widget 相关的代码的典型结构如下。
如您所见,我们可以将一些额外的参数传递给它的构造函数。 但是,请记住,这些参数不会在以后阶段发生变化,只能按原样使用。
1 | class MyAppStatelessWidget extends StatelessWidget { |
StatelessWidget 的另一个方法 createElement 也可以被复写,但你几乎不会这样做。 唯一需要被复写的是 · build 函数。
这种无状态 Widget 的生命周期很简单:
- 初始化
- 通过
build进行渲染
有状态 StatefulWidget
某些 Widge 需要处理一些在 Widget 生命周期内会发生变化的内部数据。
这些 Widget 保存的数据集在 Widget 的生命周期中可能会有所不同,这样的数据集被称为为 State。
这样的 Widget 被称为有状态 Widget。
此类 Widget 的示例可以是复选框列表,也可以是根据条件禁用的 Button。
State
State 定义 StatefulWidget 实例的 行为 部分。
它包含 Widget 交互 的信息:
- 行为
- 布局
施加于 State 的任何更改都会强制
Widget重建。
State 与 Context 之间的关系
对于有状态 Widgets,State 与 Context 相关联。 此关联是永久性的,State 对象永远不会更改其 Context。
即使可以在 Widgets 树内移动 Widget Context ,State 仍将与该 Context 相关联。
当 State 与 Context 关联时,State 被视为已挂载。
至关重要:
由于
State对象与Context相关联,这意味着State对象不能(直接)通过另一个Context访问! (我们将在稍后讨论这个问题)。
有状态 Widget 生命周期
基本概念已经介绍过了,是时候深入了解了。
由于本文的主要意图是用 “变量” 数据来解释 State 的概念,因此会故意跳过某些与 Stateful Widget 可复写方法相关的任何解释,这些方法与此没有特别的关系。 这些可复写的方法是 didUpdateWidget,deactivate,reassemble。
1 | class MyStatefulWidget extends StatefulWidget { |
下面时序图显示了创建有状态 Widget 相关的操作。 在图的右侧,标注了 State 对象的内部状态。 同时您可以看 Context 与 State 关联的时刻,从而变为有效的(挂载)。
initState()
initState() 方法是在创建 State 对象后要调用的第一个方法(在构造函数之后)。需要执行自定义初始化内容时,需要复写此方法。 通常初始化,动画,逻辑控制等。如果重写此方法,需要先调用 super.initState() 方法。
在这个方法中,Context 可用但你还不能真正使用它,因为框架还没有完全将 State 与它相关联。
一旦 initState() 方法执行完成,State 对象完成初始化并且 Context 可用。
在State 对象的生命周期内,不会再调用 initState() 方法。即 initState() 只被调用一次。
didChangeDependencies()
didChangeDependencies() 方法是要调用的第二个方法。
在此阶段,由于 Context 可用,您可以使用它。
如果您的 Widget 链接到 InheritedWidget,并且需要初始化某些侦听器(基于 Context),则需要复写此方法。
请注意,如果您的 Widget 关联到 InheritedWidget,则每次重建此 Widget 时都会调用此方法。
如果重写此方法,则应首先调用 super.didChangeDependencies()。
build()
build(BuildContext context) 方法在 didChangeDependencies()(和didUpdateWidget)之后调用。
这是您构建 Widgets(可能还有任何子树)的地方。
每次 State 对象更改时(或者当 InheritedWidget 需要通知 “已注册” 的 Widget 时)都会调用此方法!
如果要主动重建,您可以调用 setState ( () {...}) 方法。
dispose()
当 Widget 废弃时调用 dispose() 方法。
如果需要执行一些清理(例如侦听器)工作,需要复写此方法,之后调用 super.dispose()。
有状态和无状态 Widget 如何选择
Widget无状态或有状态?如何选择。
为了回答这个问题,请问问自己:
在我的
Widget生命周期中,是否需要考虑一个变量, 将要更改,何时更改,从而强制重建Widget?
如果问题的答案是肯定的,那么您需要一个有状态 Widget,否则,您需要一个无状态 Widget。
举例:
用于显示复选框列表的
Widget。要显示复选框,您需要考虑一系列item。每个item都是一个具有title和 ‘status’ 的对象。如果单击复选框,则会切换相应的item.status。在这种情况下,您需要使用有状态
Widget来记住项目的状态,以便能够重绘复选框。单一表格, 该表格
Widget允许用户输入,并将输入之后的表格发送到服务器。在这种情况下,除非您需要在提交表单之前验证表单或执行任何其他操作,否则无状态
Widget可能就足够了。
有状态 Widget 由两部分组成
Widget 定义
1 | class MyStatefulWidget extends StatefulWidget { |
第一部分 MyStatefulWidget 通常是 Widget 的公共部分。 当您要将其添加到 Widget 树,可以实例化此Widget。此部分在 Widget 的生命周期内不会发生变化,但可以接受其相应的 State 实例参数。
请注意,在 Widget 的第一部分定义的任何变量通常在其生命周期内不会更改。
State 定义
1 | class _MyStatefulWidgetState extends State<MyStatefulWidget> { |
第二部分 _MyStatefulWidgetState 是在 Widget 的生命周期中变化的部分,并在每次应用修改时强制重建 Widget实例。 名称开头的 _ 字符表示该类对当前 .dart 文件是私有的。
如果需要在 .dart 文件之外引用此类,去掉 _ 前缀即可。
_MyStatefulWidgetState 类可以使用 widget.{变量名} 访问存储在 MyStatefulWidget 中的任何变量。 示例:widget.color
Widget 的唯一标识 - Key
在Flutter中,每个Widget都是唯一标识的。这个唯一标识由框架在构建/渲染时定义。
此唯一标识对应为可选的 Key 参数。 如果省略,Flutter将为您生成一个。
在某些情况下,您可能需要使用此 Key,以便可以通过 Key 访问 Widget。
为此,您可以使用以下类之一:GlobalKey,LocalKey,UniqueKey 或 ObjectKey。
GlobalKey确保 Key 在整个应用程序中是唯一的。
使用 Widget 的唯一标识 Key:
1 | GlobalKey myKey = new GlobalKey(); |
第二部分, 如何访问 State
如前所述,State 关联一个 Context,Context 关联一个 Widget 实例。
1. Widget 本身
从理论上讲,唯一能够访问状态的是 Widget State 本身。
在这种情况下,没有困难。 Widget State 类访问其任何内部变量。
2. 子 Widget
有时候,父 Widget 可能需要根据其子 Widget 的状态执行特定任务。
在Flutter中,每个Widget都有一个唯一的标识,由框架在构建/渲染时确定。
如前所示,您可以使用 key 参数强制标识 Widget,
1 | ... |
一旦确定,父 Widget 可以通过以下方式访问其子 Widget 的状态:
myWidgetStateKey.currentState
让我们考虑一个基本示例,当用户点击按钮时显示 SnackBar。 由于 SnackBar 是 Scaffold的子 Widget,它不能直接访问 Scaffold 的任何其他子 Widget。 因此,访问它的唯一方法是 ScaffoldState,公开一个公共方法来显示 SnackBar。
1 | class _MyScreenState extends State<MyScreen> { |
3. Widget 祖先
1. 带状态的 Widget(红色)需要暴露其状态
为了公开它的状态,Widget需要在创建时记录它,如下所示:
1 | class MyExposingWidget extends StatefulWidget { |
2. Widget State 需要暴露 getters/setters
为了让其他 Widget可以 设置/获取 State属性,Widget State 需要通过以下方式授权访问:
- public 属性 (不推荐)
- getter / setter
例子:
1 | class MyExposingWidgetState extends State<MyExposingWidget>{ |
3. 蓝色 Widget 获得 State 引用
1 | class MyChildWidget extends StatelessWidget { |
这个解决方案很容易实现,但子 Widget 无法知道它何时需要重建,
它必须等待重建才能刷新其内容,这将导致UI无法及时刷新。
下一节将讨论 Inherited Widget 概念,它可以解决这个问题。
InheritedWidget
简而言之,InheritedWidget 允许在 Widget 树中有效地传递(和共享)信息。
InheritedWidget 是一个特殊的 Widget,您可以将其插入 Widget 树中,作为一个 Widget 子树的父节点。 该子树的所有 Widget 都能够访问 InheritedWidget 公开的数据。
基础知识
为了解释 InheritedWidget,让我们考虑以下代码:
1 | class MyInheritedWidget extends InheritedWidget { |
此代码定义了一个名为 MyInheritedWidget 的 Widget,目的是 “共享” 所有 Widget 中的某些数据。
如前所述,为了能够传递/共享某些数据,需要将 InheritedWidget 定位在 Widget 树的顶部,这解释为什么 InheritedWidget 构造函数设置 Widget 为 @required 。
静态方法 MyInheritedWidget(BuildContext context) 允许所有子 Widget 获取最近包含 Context 的 MyInheritedWidget 实例(参见后面的内容)。
最后重写 updateShouldNotify 方法,用于设置 InheritedWidget 是否必须通知所有子 Widget(已注册/已订阅),如果数据发生修改(请参阅下文)。
因此,我们需要将它放在树节点上,如下所示:
1 | class MyParentWidget... { |
子 Widget 如何访问 InheritedWidget 的数据
在构建子 Widget 时,将获得 InheritedWidget 的引用,如下所示:
1 | class MyChildWidget... { |
如何在 Widget 之间进行交互 ?
思考下面 Widget 树。
为了说明一种交互方式,我们假设如下:
Widget A是一个按钮,点击时将货物添加到购物车;Widget B是一个文本,显示购物车中商品数量;Widget C位于Widget B旁边,是一个文本;- 我们希望在按下
Widget A时自动在Widget B购物车中显示正确数量的项目,但我们不希望重建Widget C,InheritedWidget应用场景正式于此!
main.dart:
1 | import 'package:flutter/material.dart'; |
inherited.dart
1 | import 'package:flutter/material.dart'; |
Widgets:
1 | class WidgetA extends StatelessWidget { |
解释说明
在这个非常基本的例子中,
_MyInherited是一个InheritedWidget,每次我们点击Widget A按钮添加一个Item时都会被重新创建。MyInheritedWidget是一个Widget,其状态包含Items列表。 可以通过MyInheritedWidgetState of(BuildContext context)静态方法访问此状态。MyInheritedWidgetState公开一个 getter(itemsCount)和一个addItem方法,以便它们可以被Widget使用。- 每次我们将一个
Item添加到State,MyInheritedWidgetState都会重建。 MyStatefulWidget类只是构建一个Widget树,将MyInheritedWidget作为树的根节点。WidgetA是一个简单的RaisedButton,当按下它时,调用MyInheritedWidget的addItem方法。WidgetB是一个简单的文本,显示MyInheritedWidget的item数。
这一切是如何运作 ?
注册 Widget 以供以后通知
当子 Widget 调用 MyInheritedWidget.of(context)时,它调用 MyInheritedWidget 的以下方法,将 ‘Context’ 作为参数进行传递。
1 | static MyInheritedWidgetState of(BuildContext context) { |
在内部,除了简单地返回 MyInheritedWidgetState 的实例之外,它还向 Widget 订阅更改通知。
在场景后面,对这个静态方法的简单调用实际上做了两件事:
Widget被自动添加到订阅列表中,当对_MyInherited进行修改时, 将自动重建。MyInheritedWidgetState中引用的数据将返回给使用者。
由于 Widget A 和 Widget B 都已使用 InheritedWidget 订阅,因此如果对 _MyInherited 进行修改,即当单击 Widget A 的 RaisedButton 时,大致操作流程如下:
- 调用
MyInheritedWidgetState的addItem方法。 MyInheritedWidgetState.addItem方法将新项添加到List。- 调用
setState()重建MyInheritedWidget。 - 使用
List的新内容创建_MyInherited的新实例。 _MyInherited记录新State。- 作为
InheritedWidget,它检查是否需要通知订阅者(答案是需要)。 - 它遍历整个订阅者列表(这里是
Widget A和Widget B)并要求他们重建。 - 由于
Wiget C不是订阅者,因此不会重建。
Widget A 和 Widget B 都重建了,而重建 Wiget A 却没用,因为它没有任何改变。如何防止这种情况发生?
访问 InheritedWidget 时阻止某些 Widget 重建
Widget A 也被重建的原因是访问 MyInheritedWidgetState 的方式。
如前所述,调用 context.inheritFromWidgetOfExactType() 方法会自动将 Widget 订阅到订阅列表中。
防止此自动订阅同时仍允许 Widget A 访问 MyInheritedWidgetState 的解决方案是更改MyInheritedWidget 的静态方法,如下所示:
1 | static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){ |
添加额外布尔参数:
- 如果
rebuild参数为true(默认情况下),Widget将被添加到订阅者列表中。 - 如果
rebuild参数为false,我们仍然可以访问数据,但不使用InheritedWidget的内部实现。
因此,要完成解决方案,我们还需要稍微更新 Widget A 的代码,如下所示(我们添加 false 参数):
1 | class WidgetA extends StatelessWidget { |
现在,按下 button 不会重建 Widget A.
总结
通篇,我们介绍了 Flutter 的核心概念, Widget, State, Context, InheritedWidget。
并且,完成了 State 之间交互的介绍。并使用 InheritedWidget 进行了 Demo制作。希望这篇文章,能够帮助您,深入了解 Flutter。