Flutter中几乎所有的对象都是一个Widget。它不仅可以表示UI元素,也可以表示一些功能性的组件。Flutter中真正代表屏幕上显示元素的类是Element
,也就是说Widget只是描述Element
的配置数据。Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element
。
StatelessWidget
StatelessWidget
用于不需要维护状态的场景,它通常在build
方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。
1 | class Echo extends StatelessWidget { |
Context
context
是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法
1 | Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>(); |
StatefulWidget
StatefulWidget
也是继承自Widget
类,并重写了createElement()
方法,不同的是返回的Element
对象并不相同;另外StatefulWidget
类中添加了一个新的接口createState()
。
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:
- 在widget 构建时可以被同步读取。
- 在widget生命周期中可以被改变,当State被改变时,可以手动调用其
setState()
方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build
方法重新构建widget树,从而达到更新UI的目的。
1 | class CounterWidget extends StatefulWidget { |
页面启动时:
1 | I/flutter ( 5436): initState |
点击按钮热重载时:
1 | I/flutter ( 5436): reassemble |
在widget树中移除CounterWidget
时:
1 | I/flutter ( 5436): reassemble |
各个回调函数:
initState
:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType,原因是在初始化完成后,Widget树中的
InheritFromWidget也可能会发生变化,所以正确的做法应该在在
build()方法或
didChangeDependencies()`中调用它。didChangeDependencies()
:当State对象的依赖发生变化时会被调用;例如:在之前build()
中包含了一个InheritedWidget
,然后在之后的build()
中InheritedWidget
发生了变化,那么此时InheritedWidget
的子widget的didChangeDependencies()
回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。build()
:此回调现在应该已经相当熟悉了,它主要是用于构建Widget子树的,会在如下场景被调用:- 在调用
initState()
之后。 - 在调用
didUpdateWidget()
之后。 - 在调用
setState()
之后。 - 在调用
didChangeDependencies()
之后。 - 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
- 在调用
reassemble()
:此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。didUpdateWidget()
:在widget重新构建时,Flutter framework会调用Widget.canUpdate
来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate
返回true
则会调用此回调。正如之前所述,Widget.canUpdate
会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()
就会被调用。deactivate()
:当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()
方法。dispose()
:当State对象从树中被永久移除时调用;通常在此回调中释放资源。
获取State对象
context
对象有一个findAncestorStateOfType()
方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。
1 | // 查找父级最近的Scaffold对应的ScaffoldState对象 |
一般来说,StatefulWidget的状态是私有的,但是findAncestorStateOfType也是通用的。在Flutter开发中便有了一个默认的约定:如果StatefulWidget的状态是希望暴露出的,应当在StatefulWidget中提供一个of
静态方法来获取其State对象,开发者便可直接通过该方法来获取;如果State不希望暴露,则不提供of
方法。
1 | ScaffoldState _state=Scaffold.of(context); |
通过GlobalKey
Flutter还有一种通用的获取State
对象的方法——通过GlobalKey来获取。
-
给目标
StatefulWidget
添加GlobalKey
。1
2
3
4
5
6
7//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
) -
通过
GlobalKey
来获取State
对象1
_globalKey.currentState.openDrawer()
GlobalKey是Flutter提供的一种在整个APP中引用element的机制。如果一个widget设置了GlobalKey
,那么我们便可以通过globalKey.currentWidget
获得该widget对象、globalKey.currentElement
来获得widget对应的element对象,如果当前widget是StatefulWidget
,则可以通过globalKey.currentState
来获得该widget对应的state对象。
注意:使用GlobalKey开销较大,如果有其他可选方案,应尽量避免使用它。另外同一个GlobalKey在整个widget树中必须是唯一的,不能重复。
基础 widgets
Text
Text widget 可以用来在应用内创建带样式的文本。
1 | body: Center( |
Row
在水平方向创建灵活的布局。
Column
在垂直方向创建灵活的布局。
Stack
Stack
widget 不是线性(水平或垂直)定位的,而是按照绘制顺序将 widget 堆叠在一起。你可以用 Positioned
widget 作为Stack
的子 widget,以相对于 Stack
的上,右,下,左来定位它们。 Stack 是基于 Web 中的绝对位置布局模型设计的。
1 | body: Stack( |
Container
Containerwidget 可以用来创建一个可见的矩形元素。 Container 可以使用 BoxDecoration来进行装饰,如背景,边框,或阴影等。
Container` 还可以设置外边距、内边距和尺寸的约束条件等。
1 | body: Container( |
Material组件
Flutter提供了一套丰富的Material组件,如:Scaffold
、AppBar
、FlatButton
等。要使用Material 组件,需要先引入它:
1 | import 'package:flutter/material.dart'; |
Cupertino组件(iOS)
Flutter也提供了一套丰富的Cupertino风格的组件。
1 | //导入cupertino widget库 |
状态管理
管理状态的最常见的方法:
- Widget管理自己的状态。
- Widget管理子Widget状态。
- 混合管理(父Widget和子Widget都管理状态)。
如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
- 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
Widget管理自身状态
_TapboxAState 类:
- 管理TapboxA的状态。
- 定义
_active
:确定盒子的当前颜色的布尔值。 - 定义
_handleTap()
函数,该函数在点击该盒子时更新_active
,并调用setState()
更新UI。 - 实现widget的所有交互式行为。
1 | class TapboxA extends StatefulWidget { |
父Widget管理子Widget的状态
对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。
ParentWidgetState
类:
- 为TapboxB 管理
_active
状态。 - 实现
_handleTapboxChanged()
,当盒子被点击时调用的方法。 - 当状态改变时,调用
setState()
更新UI。
TapboxB 类:
- 继承
StatelessWidget
类,因为所有状态都由其父组件处理。 - 当检测到点击时,它会通知父组件。
1 | class ParentWidget extends StatefulWidget { |
1 | class TapboxB extends StatelessWidget { |
混合状态管理
_ParentWidgetStateC
类:
- 管理
_active
状态。 - 实现
_handleTapboxChanged()
,当盒子被点击时调用。 - 当点击盒子并且
_active
状态改变时调用setState()
更新UI。
_TapboxCState
对象:
- 管理
_highlight
状态。 GestureDetector
监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。- 当按下、抬起、或者取消点击时更新
_highlight
状态,调用setState()
更新UI。 - 当点击时,将状态的改变传递给父组件。
全局状态管理
当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,APP中依赖应用语言的组件能够重新build一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
- 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的
initState
方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)
方法重新build
一下自身即可。 - 使用一些专门用于状态管理的包,如Provider、Redux,读者可以在pub上查看其详细信息。