在 Flutter的世界里,一切都是 Widget,而 Widget有两个子类 StatelessWidget和StatefulWidget,下面一起学习这两类Widget 是什么、有什么区别、使用时如何选择。
在了解 StatelessWidget 和 StatefulWidget之前先了解 Flutter 中的UI编程范式。
UI变成范式
UI编程范式:如何调整一个控件的展示样式。有两种方式,命令式编程和声明式编程。
命令式编程:通过命令精准的控制控件的属性,如 Android 中,修改TextView 文本内容,textView.text = “newText”。
声明式编程:其核心设计思想就是将视图和数据分离,Flutter中就是使用这声明式。除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为Widget绑定数据集,使 Widget 根据这个数据集完成渲染。
声明式编程的优点:当需要改变页面的文案时,只需要改变数据集中的文案,并通知Flutter框架触发widget重新渲染即可。这样一来就不用精准关注UI编程的各个过程细节,只需要维护好数据集即可。
StatefulWidget
StatelessWidget:无状态的Widget,在初始化的时候就可以确定Widget 的显示样式,之后不会再发生变化。如 Text、Container、Row、Column 等。
看看Text 的源码:
class Text extends StatelessWidget {
//构造方法及属性声明部分
const Text(this.data, {
Key key,
this.textAlign,
this.textDirection,
//其他参数
...
}) : assert(data != null),
textSpan = null,
super(key: key);
final String data;
final TextAlign textAlign;
final TextDirection textDirection;
//其他属性
...
@override
Widget build(BuildContext context) {
...
Widget result = RichText(
//初始化配置
...
)
);
...
return result;
}
}
在构造方法将其属性列表赋值后,build 方法随即将子组件 RichText 通过其属性列表(如文本 data、对齐方式 textAlign、文本展示方向 textDirection 等)初始化后返回,之后 Text 内部不再响应外部数据的变化。
使用场景:如果显示样式能在一开始就确定,之后不会响应数据的变化,就可以使用 StatelessWidget。如 每一个页面的 title 只初始化一次,后面不再发生变化,提示错误信息的dialog 等。
StatefulWidget
StatelessWidget:有状态的Widget,在初始化的时候就不可以确定Widget 的显示样式,之后可以响应数据集的变化,重新渲染UI。如,处理用户的交互(比如,用户点击按钮)或其内部数据的变化(比如,网络数据回包),并体现在 UI 上。
看看一个例子:Image的源码
class Image extends StatefulWidget {
//构造方法及属性声明部分
const Image({
Key key,
@required this.image,
//其他参数
}) : assert(image != null),
super(key: key);
final ImageProvider image;
//其他属性
...
@override
_ImageState createState() => _ImageState();
...
}
class _ImageState extends State<Image> {
ImageInfo _imageInfo;
//其他属性
...
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
...
@override
Widget build(BuildContext context) {
final RawImage image = RawImage(
image: _imageInfo?.image,
//其他初始化配置
...
);
return image;
}
...
}
首先可以看到在 StatefulWidget 中没有 build 方法,而是多了一个 createState方法;通过 createState 方法把对视图的构建工作交给了 _ImageState;在 _ImageState 中,视图的信息通过 ImageInfo 来获取的。
ImageInfo 数据变化,更新Image流程:
- 当 ImageInfo 数据发生变化时,State 对象通过 _handleImageChanged 方法监听到 _imageInfo 属性发生了变化
- 调用 _ImageState 类的 setState 方法通知 Flutter 框架:“我这儿的数据变啦,请使用更新后的 _imageInfo 数据重新加载图片!”。
- Flutter 框架则会标记视图状态,更新 UI。
到这来可以发现 StatefulWidget 既可以响应数据的变化,又可以展示静态UI,StatelessWidget是不是有点多余了?
先看看widget 的更新机制:
Widget 是不可变的,更新则意味着销毁 + 重建(build)。
StatelessWidget 是静态的,一旦创建则无需更新;
而对于 StatefulWidget 来说,在 State 类中调用 setState 方法更新数据,会触发视图的销毁和重建,也将间接地触发其每个子 Widget 的销毁和重建。
这就意味着,如果我们的根布局是一个 StatefulWidget,在其 State 中每调用一次更新 UI,都将是一整个页面所有 Widget 的销毁和重建。
虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但是大量 Widget 对象的销毁重建是无法避免的。如果某个子 Widget 的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降。
所有 正确评估要不要使用StatefulWidget,是提高Flutter渲染性能最直接的手段。
StatelessWidget 和 StatefulWidget 如何选择?
一句话:只是其静态展示,使用StatelessWidget,否则 使用StatefulWidget。
Widget的生命周期
Flutter 中的 Widget 也存在生命周期,并且通过 State 来体现。(是对StatefulWidget来说的)
State 生命周期
State 的生命周期,指的是在用户参与的情况下,其关联的 Widget 所经历的,从创建到显示再到更新最后到停止,直至销毁等各个过程阶段。
从图中可以看到 state 的生命周期主要有三个过程:创建、更新和销毁。
创建
State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染
构造方法:state 生命周期的起点,由 StatefulWidget#createState方法创建,在这里可以接收到从外部传入的初始化数据。
initState,在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作。类似Android 中activity的onCreate方法。
didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。
build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。
更新
如何更新的?
通过三个方法:setState、didchangeDependencies 与 didUpdateWidget。
setState:一般是当数据发生变化后,通过该方法来通知flutter 框架来更新UI,开发中常用。
didchangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。如系统 语言的切换等。
didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。
销毁
组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。
deactivate:当组件的可见状态发生变化时,会调用。
dispose:当 State 被永久地从视图树中移除时,这个阶段,组件就要被销毁了,所以我们可以在这里进行最终的资源释放、移除监听、清理环境,等等。类似 Android 中activity 的onDestory方法。
上面就是一state 的生命周期,和activity 的生命周期很类似。下面使用一张表格来总结,方便对比和理解。
| 方法名 | 功能 | 调用时期 | 调用次数 |
|---|---|---|---|
| 构造方法 | 接收父widget传递过来初始化UI的数据 | 创建State | 1 |
| initState | 预渲染相关的初始化工作 | State 被插入视图树时 | 1 |
| didchangeDependencies | 处理State 依赖对象发生变化时 | initState后或State 依赖关系发生变化时 | >=1 |
| build | 构建视图 | State 准备好数据渲染时 | >=1 |
| setState | 触发视图重建 | 需要更新UI | >=1 |
| didchangeDependencies | 处理widget的配置发生变化 | 父widget setState 触发子widget重建 | >=1 |
| deactivate | 组件被移除 | 组件不可见 | >=1 |
| dispose | 组件被销毁 | 组件被永远移除 | 1 |
APP的生命周期
上面学习了State 的生命周期,而 App 的生命周期,则定义了 App 从启动到退出的全过程。在开发中常常需要在对应的APP 生命周期中处理一些事情,如 Android 中在 appLication.onCreate 方法中做一些出事操作,前台切换都后台取消网络请求等。在Flutter 中如何处理呢?
利用 WidgetsBindingObserver 类来完成生命周期的监听。
abstract class WidgetsBindingObserver {
//页面pop
Future<bool> didPopRoute() => Future<bool>.value(false);
//页面push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
//系统窗口相关改变回调,如旋转
void didChangeMetrics() { }
//文本缩放系数变化
void didChangeTextScaleFactor() { }
//系统亮度变化
void didChangePlatformBrightness() { }
//本地化语言变化
void didChangeLocales(List<Locale> locale) { }
//App生命周期变化
void didChangeAppLifecycleState(AppLifecycleState state) { }
//内存警告回调
void didHaveMemoryPressure() { }
//Accessibility相关特性回调
void didChangeAccessibilityFeatures() {}
}
在 WidgetsBindingObserver 中有很多接口回调,我们通过给 WidgetsBinding 的单例对象设置监听器,就可以监听对应的回调方法。具体使用可以参考官方文档
这里我们最关心的是 App 生命周期的回调 didChangeAppLifecycleState,和帧绘制回调 addPostFrameCallback 与 addPersistentFrameCallback。
didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。
- resumed:可见的,并能响应用户的输入。
- inactive:处在不活动状态,无法处理用户响应。
- paused:不可见并不能响应用户的输入,但是在后台继续活动中。
来一个简单的例子:在新建的Flutter_Demo中,在 initState注册监听,在dispose 移除监听,然后分别从后台切换到前台、前台切换到后台,看看有什么变化。
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme
.of(context)
.textTheme
.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
print("didChangeAppLifecycleState::state=$state");
}
}
- 从前台到后台,按back键:AppLifecycleState.inactive->AppLifecycleState.paused->AppLifecycleState.detached
- 从前台到后台,按home键:AppLifecycleState.inactive->AppLifecycleState.paused
- 从后台到前台:AppLifecycleState.resumed
帧绘制回调
WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求:
- 单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。
在开发中有这样的需求,当页面可见后,再做其他操作,这时候就可以通过监听,得到第一帧回调后,再做其他事情。
WidgetsBinding.instance.addPostFrameCallback((_){ print("单次Frame绘制回调");//只回调一次 });
- 实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。
WidgetsBinding.instance.addPersistentFrameCallback((_){
print("实时Frame绘制回调");//每帧都回调
});
使用的场景,一个小例子,在画泡泡上升的动画,我可以用Timer来画,但是我手机性能好的话,一帧结束了。还有等待一段时间才绘制下一帧;手机性能差的话,我前一帧都没画完呢,你就开始下一帧了,这不是要卡死的节奏吗?
这时候就可以通过 addPersistentFrameCallback 来监听,完成这一帧后,再绘制下一帧,这比定时器好多了。