Flutter - Widget-Context-Stage
读前须知:此篇文章基本上是Widget - State - Context - InheritedWidget的翻译并且删减了部分我个人觉得没有意义的文字,保留下来的部分也不会逐字逐句精确翻译,所以其实强烈推荐阅读英文原文。
以下文章里面除了第一张思维导图是本人所作之外,凡是出现的示例代码和图片都是上面提到的英文文章里面的。本人在这里对此表示感激和愧疚,如果涉及到侵权,本人会立即删掉整篇文章。
All the example codes and images used in this article are from Widget - State - Context - InheritedWidget excepting the first one. If this is of tort, I will delete this article immediately.
正文开始:
Widget, State 和Context是每一个flutter开发者必须要完全理解的概念,但是具体来说要理解哪些知识呢?这篇文章会就以下几个知识点进行讲解:
1: Stateful和Stateless widget的差别
2:什么是Context
3: 什么是State以及怎么使用
4:一个context和他的state之间的关系
5:InheritedWidget以及在在Widget树里面怎么传递信息
6:rebuild的概念
接下来文章会按照以下结构去展开:
PS:由于此篇的篇幅很长,此思维导图里面的第二部分(左边的’怎样获取State‘)的篇幅也会很长,所以第二部分会放到下一篇文章讲解。
一:基本概念解释
1:什么是Widget
Widget即组件。在flutter里面几乎万物都是Widget,跟我们常说的component是同一个概念。
2: 什么是Widget tree
Widget按照树形结构组合的产物就是Widget tree。这个Widget tree的每个节点上又是一个Widget。包含其他组件的组件叫做父组件,被其他组件包含的组件叫做子组件。比如下面一段代码:
@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 tree的话就如下图所示:
3:什么是Context
一个context标识了一个widget在widget tree的结构中是在哪个地方被build的。简而言之就是一个context标明了一个widget是挂载在widget tree的那个节点的。
一个context只属于一个widget。
假如一个widget A有子组件,那么widget A的context会变成子组件的父context。
以上文提到的widget tree为例子,假如我们画出它的context,如下图所示(一个颜色代表一个context):
Context Visibility(上下文可见性)
只在其自己的context或者在其父context可见。
从以上描述,我们可以轻易地找到一个widget的父widget,例如:
Scaffold > Center > Column > Text:
context.ancestorWidgetOfExtractType(Scaffold) => 会找到第一个沿着Text的context一路向上而遇到的第一个Scaffold.
从一个父组件,也能找到子组件,但是不建议这么做(稍后会讨论到)。
4:Widget的分类
在Flutter里面有2类widget:
- Stateless Widget
- Stateful Widget
从字面意思上可以理解Stateless Widget就是无状态的组件,Stateful Widget是有状态的组件,接下来我们对二者做更具体的讲解。
Stateless Widget
有些组件只依赖于他们自己的配置信息,这些配置信息一般是由他们的父组件在build他们的时候提供。
换句话说,这些组件一旦创建之后,就不用担心任何变化。
这类型的组件就是Stateless Widget.
典型的例子比如Text, Row, Column, Container等,对于这些组件来说在build的时候,我们只需要传一些参数给他们就好。
一个Stateless Widget只能被build一次,意思就是一旦build,不会因为任何的事件或者用户行为而重新build。
Stateless Widget lifecycle
下面是一个典型的Stateless Widget的代码结构。
如我们所见,我们传递一些阐述给它的构造函数,但是请记住,这些参数不会再之后被改变了,所以一般你也会看到这些参数是被定义为final的。
class MyStatelessWidget extends StatelessWidget { MyStatelessWidget({ Key key, this.parameter, }): super(key:key); final parameter; @override Widget build(BuildContext context){ return new ... } }
虽然有另一个方法(createElement)可以被overridden,但是一般没人会用到它。唯一一个需要被override的方法就是build().
Stateless widget的生命周期是直接而简单的,如下所示:
- 初始化
- 通过build()方法渲染
Stateful Widget
一些其他的组件所拥有的数据会在组件生命周期内产生变化,这些数据就变成了dynamic(动态的)的。
这些被组件拥有的会在组件的生命周期内改变的数据的列表,我们叫做State。
而拥有以上特点的组件,我们就叫做Stateful Widget。
Stateful Widget的例子就好比一个用户可以选择的Checkboxes的列表或者一个会根据某种条件而disabled的Button。
5: 什么是State
一个State定义了一个StatefulWidget的“行为”部分。
State包含了以下旨在与一个组件交互的:
- 行为(behaviour)
- UI布局(layout)
任何对State的改变都会导致这个组件的rebuild。
6:State和Context之间的关系
对Stateful Widgets而言,一个State是和一个Context是绑定的,而且这种绑定关系是永久的,一个State永远不会改变他的Context。
即使一个组件的Context在Widget tree上发生了移动,这个State还是会依然和那个Context绑定在一起。
非常重要的知识点:
因为一个State对象和一个Context是绑定的,这就意味着这个State对象不能从其他的Context下直接被获取到(这一点之后会讨论到)
7:StatefulWidget的生命周期(lifecycle)
前面已经介绍和很多StatefulWidget的相关概念和基础知识,接下来我们来了解一下StatefulWidget的生命周期,这里不会介绍全部的生命周期,先只挑几个重要的,与本篇文章的主旨相关的几个来讲。先看下下面一个StatefulWidget的例子:
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 ... } }
下面的这个图展示了(一个简化的版本)一个StatefuleWidget在创建的时候,内部的一些列行为。在这个图的右边你可以看到一个State对象的内部状态变化以及最右边的Context是在什么时候和State产出联系而因此变为可用的。
initState()
initState()是在构造函数之后的第一个被调用的生命周期方法。当你需要再初始化阶段做一个额外的操作的时候,你需要override它。典型的在initState()方法里额外的操作比如动画,或者某些数据准备。假如你override initState(),记得调用super.initState()并且这行代码要放在initState()方法体的最前面。意思就是你得让super.initState()执行了之后再去执行你额外的初始化工作。
在这个阶段,一个context是存在的,但是你并不能真正地使用它,因为这时候context和state还没有完成绑定。
一旦initState()执行完毕,State对象就初始化好了,context也就可以被使用了。
initState()在整个生命周期内只会被调用一次。
didChangeDependencies()
didChangeDependencies()是在生命周期里第二个被调用的方法。
这个阶段,context已经可以被使用了。
假如你的组件是链接到了InheritedWidget,根据context你需要初始化一些listeners(监听者),通常你需要override这个方法。
注意,如果某个组件是链接了InheritedWidget,那么这个组件每次重建(rebuild),didChangeDependencies()都会被调用。
假如你要override这个方法,你应该首先调用super.didChangeDependencies().
build()
build()跟在didChangeDependencies()(和didUpdateWidget())之后被调用。
这个方法就是用来构建你的组件的。
每一次State对象发生变化(或者InheritedWidget需要通知它的注册者)时,build()方法都会被调用。
通常,我们通过调用setState((){…})来改变State对象,强制build()被调用,从而重新build我们的widget。
dispose()
dispose()在这个组件被销毁的时候被调用。
一般我们override这个方法,可以在组件被销毁的时候做一个清理工作。
override dispose()记得调用super.dispose()并且把它放在方法体的最后。
8: StatelessWidget和StatefulWidget之间的抉择
既然在Flutter里面有StatelessWidget和StatefulWidget这两种类型的组件,那在这二者之间如何抉择呢?
记住标准就是:
在这个组件的生命周期内,是否有会变化的数据,这个组件是否需要rebuild?
如果答案是yes,那你就需要一个StatefulWidget而不会StatelessWidget。
9:StatefulWidget的2个组成部分
组件的构造函数部分
class MyStatefulWidget extends StatefulWidget { MyStatefulWidget({ Key key, this.color, }): super(key: key); final Color color; @override _MyStatefulWidgetState createState() => new _MyStatefulWidgetState(); }
这部分是一个StatefulWidget的public部分。这部分不会在一个组件的生命周期内发生改变,它只是接收一些参数以便它的State可以使用。比如上面这个例子里面的color这个参数。
Widget State定义部分
class _MyStatefulWidgetState extends State<MyStatefulWidget> { ... @override Widget build(BuildContext context){ ... } }
_MyStatefulWidgetState是这个Widget在其生命周期内变化的部分,也是使得这个Widget能够rebuild的部分。
_MyStatefulWidgetState通过widget.{name of the variable}可以获取存在MyStatefulWidget内的任意变量。例如这里,可以通过widget.color获取color变量。
10:Widget的唯一标识-key
在Flutter里面,每一个组件都是唯一标识的,这个唯一标识是在build的时候被定义的。
这个唯一的标识就是组件的可选参数:Key. 加入key缺省了,系统会默认给你创建一个。
在某些情形下,你必须强制制定key,以便你可以通过这个key获取到这个组件。
你可以通过下面的一些helper来达到上面的目的:GlobalKey, LocalKey, UniqueKey或者ObjectKey。
GlobalKey保证这个key在整个application里面都是唯一的。
以下的例子就是保证myKey在整个application都是唯一的:
GlobalKey myKey = new GlobalKey(); ... @override Widget build(BuildContext context){ return new MyWidget( key: myKey ); }
PS:由于此篇的篇幅已经够长,之前的思维导图里面的第二部分的篇幅也会很长,所以第二部分会放到下一篇文章讲解。