前言
状态管理是什么?简单的来说,就是当某个状态发生变化的时候,告知该状态的监听者,让状态所监听的属性随之而改变,达到UI层随着数据层变化而变化的效果。在Flutter中的状态(State)是一个组件的UI数据模型,是组件渲染时的依据。
在Flutter中一切皆Widget,不论是StatelessWidget还是StatefulWidget,它们都是直接继承了Widget类。而在页面实现的过程中,我们几乎离不开它们,Widget本身是不可变的,一旦初始化后,其属性就不再改变。
StatelessWidget和StatefulWidget其实没有本质的区别,它们的所有属性都是不可变的,所以无法更新,除非重新new一个新的Widget去替换它们。与之不同的是,StatefulWidget拥有一个可变的State,即它们的区别就在于这个可变的State。
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
...
}
Flutter有三颗树,Widget/Element/RenderObject,Widget是整个UI界面的配置,其中 Element 是通过Widget生成的,主要作用是维护UI元素的树形结构,并将Widget和RenderObject进行关联。RenderObject 其主要作用是负责界面的绘制和布局,是属于底层系统,Flutter开发者一般不需要直接操作该树。Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。
为了近一步了解Widget,下面我们用三种方式来实现一个简单相同的自定义文本组件,代码实现:
import 'package:flutter/material.dart';
/// 方式一:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class TextWidget extends Widget {
final String text;
const TextWidget(this.text, {super.key});
@override
StatelessElement createElement() => StatelessElement(_build());
_build() {
return InkWell(
child: Padding(padding: const EdgeInsets.all(16), child: Text(text)),
onTap: () {
print('点击文本:$text');
},
);
}
}
/// 方式二:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class TextWidget2 extends StatelessWidget {
final String text;
const TextWidget2(this.text, {super.key});
@override
Widget build(BuildContext context) {
return InkWell(
child: Padding(padding: const EdgeInsets.all(16), child: Text(text)),
onTap: () {
print('点击文本:$text');
},
);
}
}
/// 方式三:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class TextWidget3 extends StatefulWidget {
final String text;
const TextWidget3(this.text, {super.key});
@override
State<StatefulWidget> createState() => _TextWidget3State();
}
class _TextWidget3State extends State<TextWidget3> {
int _count = 0;
@override
Widget build(BuildContext context) {
return InkWell(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(widget.text + _count.toString())),
onTap: () {
_count++;
if (_count > 9) {
_count = 0;
}
print('点击文本:${widget.text}');
setState(() {});
},
);
}
}
/// 通过 RenderObject 自定义 Widget >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
class TextWidget4 extends LeafRenderObjectWidget {
final String text;
const TextWidget4(this.text, {super.key});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomObject(text);
}
@override
void updateRenderObject(BuildContext context, RenderBox renderObject) {
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox {
final String text;
RenderCustomObject(this.text);
@override
bool get sizedByParent => true;
@override
void performResize() {
size = constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: text,
style: const TextStyle(fontSize: 16, color: Colors.black),
);
textPainter.layout();
textPainter.paint(context.canvas,
Offset((size.width - textPainter.width) / 2, offset.dy));
}
}
通过以上代码,我们可以发现不论是哪种方式,都能实现同样的功能,它们最终都会创建一个独立的Element节点,不同的是StatefulWidget会提供一个createState()的方法给开发者创建它可变的状态,然后开发者通过调用setState触发build函数重建
状态管理
StatefulWidget的状态应该被谁管理?Widget本身?父 Widget ?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:
- Widget 管理自己的状态
- Widget 管理子 Widget 状态
- 混合管理(父 Widget 和子 Widget 都管理状态)
Widget管理自己的状态
以自定义隐私条款类为例子,_AgreeTermsWidgetState类:
- 管理AgreeTermsWidget的状态。
- 定义_check变量来确定勾选隐私条款的布尔值。
- 实现勾选点击onTap()函数,该函数在点击时更新_check,并调用setState()更新UI。
- 实现widget的所有交互式行为。
- 调用者不需要关心被调用者的UI状态,通过回调的形式把结果回传给调用者即可。
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// 同意隐私条款
class AgreeTermsWidget extends StatefulWidget {
final Function(bool)? callBack;
const AgreeTermsWidget({Key? key, this.callBack}) : super(key: key);
@override
State<StatefulWidget> createState() => _AgreeTermsWidgetState();
}
class _AgreeTermsWidgetState extends State<AgreeTermsWidget> {
// 是否勾选
bool _check = false;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
child:
Icon(_check ? Icons.check_box : Icons.check_box_outline_blank),
onTap: () {
setState(() {
_check = !_check;
widget.callBack?.call(_check);
});
}),
const SizedBox(width: 5),
Expanded(
child: RichText(
text: TextSpan(
text: '我已仔細閱讀並明瞭',
style: const TextStyle(color: Colors.grey),
children: <TextSpan>[
_highLightTextSpan('隱私權聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('服務條款', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('免責聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('版權保護政策', 'https://www.xxx.com/index'),
const TextSpan(
text: '等所載內容及其意義,茲同意該等條款規定,並願遵守網站現今、嗣後規範的各種規則',
style: TextStyle(color: Colors.grey),
)
]))),
],
);
}
/// 高亮文本
TextSpan _highLightTextSpan(String title, String jumpUrl) {
final TapGestureRecognizer recognizer = TapGestureRecognizer();
recognizer.onTap = () {
// 点击实现
};
return TextSpan(
text: '「$title」',
style: const TextStyle(
color: Colors.blue, decoration: TextDecoration.underline),
recognizer: recognizer);
}
}
Widget管理子Widget的状态
以自定义隐私条款类为例子,AgreeTermsWidget类:
- 继承StatelessWidget类,自身无需管理状态,所有状态都由其父组件处理。
- 当检测到点击时,它会通知父组件,父Widget接收到通知后,可以调用setState()更新UI。
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// 同意隐私条款
class AgreeTermsWidget extends StatelessWidget {
final bool check;
final Function(bool) callBack;
const AgreeTermsWidget({Key? key, this.check = false, required this.callBack})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
child:
Icon(check ? Icons.check_box : Icons.check_box_outline_blank),
onTap: () {
callBack.call(!check);
}),
const SizedBox(width: 5),
Expanded(
child: RichText(
text: TextSpan(
text: '我已仔細閱讀並明瞭',
style: const TextStyle(color: Colors.grey),
children: <TextSpan>[
_highLightTextSpan('隱私權聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('服務條款', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('免責聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan('版權保護政策', 'https://www.xxx.com/index'),
const TextSpan(
text: '等所載內容及其意義,茲同意該等條款規定,並願遵守網站現今、嗣後規範的各種規則',
style: TextStyle(color: Colors.grey),
)
]))),
],
);
}
/// 高亮文本
TextSpan _highLightTextSpan(String title, String jumpUrl) {
final TapGestureRecognizer recognizer = TapGestureRecognizer();
recognizer.onTap = () {
// 点击实现
};
return TextSpan(
text: '「$title」',
style: const TextStyle(
color: Colors.blue, decoration: TextDecoration.underline),
recognizer: recognizer);
}
}
Widget混合状态管理
以自定义隐私条款类为例子,AgreeTermsWidget类:
- 继承StatefulWidget类,内部状态由自身管理,外部状态由父组件管理
- 当检测到点击复选框时,它会通知父组件,父Widget接收到通知后,可以调用setState()更新UI。
- 当点击具体隐私条款时,它会在内部去调用setState()更新UI,刷新隐私条款文案的字体颜色。
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// 同意隐私条款
class AgreeTermsWidget extends StatefulWidget {
final bool check;
final Function(bool) callBack;
const AgreeTermsWidget(
{Key? key, required this.check, required this.callBack})
: super(key: key);
@override
State<StatefulWidget> createState() => _AgreeTermsWidgetState();
}
class _AgreeTermsWidgetState extends State<AgreeTermsWidget> {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
child: Icon(
widget.check ? Icons.check_box : Icons.check_box_outline_blank),
onTap: () {
widget.callBack.call(!widget.check);
}),
const SizedBox(width: 5),
Expanded(
child: RichText(
text: TextSpan(
text: '我已仔細閱讀並明瞭',
style: const TextStyle(color: Colors.grey),
children: <TextSpan>[
_highLightTextSpan(0, '隱私權聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan(1, '服務條款', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan(2, '免責聲明', 'https://www.xxx.com/index'),
const TextSpan(
text: '、',
style: TextStyle(color: Colors.grey),
),
_highLightTextSpan(3, '版權保護政策', 'https://www.xxx.com/index'),
const TextSpan(
text: '等所載內容及其意義,茲同意該等條款規定,並願遵守網站現今、嗣後規範的各種規則',
style: TextStyle(color: Colors.grey),
)
]))),
],
);
}
int _index = -1;
// 高亮颜色
Color _color = Colors.blue;
/// 更新颜色
_updateColor(Color color) {
setState(() {
_color = color;
});
}
/// 高亮文本
TextSpan _highLightTextSpan(int index, String title, String jumpUrl) {
final TapGestureRecognizer recognizer = TapGestureRecognizer();
recognizer.onTapDown = (details) {
_index = index;
_updateColor(Colors.orange);
};
recognizer.onTapUp = (details) {
_updateColor(Colors.blue);
};
recognizer.onTapCancel = () {
_updateColor(Colors.blue);
};
recognizer.onTap = () {
// 点击实现
};
return TextSpan(
text: '「$title」',
style: TextStyle(
color: _index == index ? _color : Colors.blue,
decoration: TextDecoration.underline),
recognizer: recognizer);
}
}
如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
- 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。
局部状态管理
Flutter局部状态管理是通过注册Element依赖实现,为此构造了InheritedWidget和InheritedModel来实现数据状态管理。InheritedWidget中内部包括了一个泛型的data类型,用于接受数据,它本身是一个StatefulWidget,用于包装child并为child提供数据,这样InheritedWidget下所有的子节点就能访问它的data。
InheritedWidget
继承自widget类,它提供了一种在 widget 树中从上到下共享数据的方式,我们可以自己实现一个of方法,这样方便调用,要从构建上下文中获取特定类型的继承小部件的最近实例,请使用:BuildContext.dependOnInheritedWidgetOfExactType
import 'package:flutter/material.dart';
// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
const InheritedProvider({
super.key,
required this.data,
required Widget child,
}) : super(child: child);
final T data;
// 自定义of方法,方便子树中的widget获取共享数据
static InheritedProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedProvider>();
}
@override
bool updateShouldNotify(InheritedProvider<T> oldWidget) {
// 数据发生变化时,返回true,表示通知子树中依赖data的Widget重新build
return oldWidget.data != data;
}
}
setState
Flutter/Dart内置支持setState,但是它的作用对象必须是一个有状态的组件,当调用setState后,会重新触发build函数,所在的Widget就会进行重新渲染,这样就实现了页面更新
Global Key通信
实现Global Key通信,跨Widget访问状态,进行内部自身刷新
GlobalKey key = GlobalKey();
key: key,
key.currentState.();
ValueListenableBuilder
ValueNotifier包含一个 T value 值,当值改变的时候,会通知它的监听来刷新UI
class _CountPageState extends State<CountPage> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: _counter,
builder: (BuildContext context, int value, Widget? child) {
return Text('$value');
}),
TextButton(
onPressed: () {
_counter.value++;
},
child: const Text('计数'))
],
);
}
@override
void dispose() {
super.dispose();
_counter.dispose();
}
}
StreamBuilder
StreamBuilder也是官方内置的一种刷新UI方式,数据封装成流通知UI变更
class _CountPageState extends State<CountPage> {
int _count = 0;
final StreamController<int> _controller = StreamController<int>();
@override
Widget build(BuildContext context) {
return Column(
children: [
StreamBuilder<int>(
stream: _controller.stream,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text('${snapshot.data}');
}),
TextButton(
onPressed: () {
_count++;
_controller.add(_count);
},
child: const Text('计数'))
],
);
}
@override
void dispose() {
super.dispose();
_controller.close();
}
}
FutureBuilder
通常用于异步编程
class _CountPageState extends State<CountPage> {
@override
Widget build(BuildContext context) {
return Column(
children: [
FutureBuilder(
future: _load(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
String data = '加载中...';
if (snapshot.connectionState == ConnectionState.done) {
// 请求完成
if (!snapshot.hasError) {
// 请求成功
if (null != snapshot.data) {
data = snapshot.data.toString();
}
}
} else {
// 请求还未结束
}
return Text(data);
},
),
],
);
}
Future _load() async {
await Future.delayed(const Duration(milliseconds: 2 * 1000));
return '测试数据';
}
@override
void dispose() {
super.dispose();
}
}
全局状态管理
当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。例如我们要实现中英文版切换/字体大小切/主题切换等全局功能,我们就需要全局状态管理处理组件之间的通信。
scoped_model
Scoped model内部使用了InheritedWidget 来共享状态,它可以将数据模型从父Widget传递到其他后代,在顶层使用ScopedModel进行包裹,在子页面获取Model进行数据渲染。
它有两种方式可以找到ScopedModel:
- 使用ScopedModelDescendant小部件
- 使用ScopedModel.of<>(context, rebuildOnChange)静态方法
注意:rebuildOnChange属性能够控制当该状态发生变化时,是否rebuild,作用等同于setState
provider
对 InheritedWidget 组件的上层封装,使其更易用,更易复用。
使用 provider 而非手动书写 InheritedWidget,有以下的优势:
- 简化的资源分配与处置
- 懒加载
- 创建新类时减少大量的模板代码
- 支持 DevTools
- 更通用的调用 InheritedWidget 的方式(参考 Provider.of/Consumer/Selector)
- 提升类的可扩展性,整体的监听架构时间复杂度以指数级增长(如 ChangeNotifier, 其复杂度为 O(N))
注意:在onPressed、OnTap、onLongPressed等事件处理程序上,我们必须使用Provider.of<T>(context,listen:false) ,它们只负责响应事件,而非更新展示,像Text等小部件会负责UI的展示。所以当不需要模型中的数据来改变UI,但是还是需要访问数据,我们需要将listen 设置为 false,默认值为true。
bloc
BLoC是一种设计模式,利用流的方式实现界面的异步渲染和重绘,这种设计模式有助于将表示与业务逻辑分开,遵循 BLoC 模式有助于提高可测试性和可重用性。这个包抽象了模式的反应方面,允许开发人员专注于编写业务逻辑。BLoC只是一种设计模式,不包含任何代码,所以要在Flutter中实现BLoC设计模式需要借助flutter_bloc这个库来完成
MobX
MobX 是一个状态管理库,可以轻松地将应用程序的反应数据与 UI 连接起来。这种接线是完全自动的,感觉非常自然。作为应用程序开发人员,您完全关注需要在 UI(和其他地方)中使用哪些反应数据,而不用担心保持两者同步。
它并不是真正的魔法,但它确实在消耗的内容(可观察对象)和消耗的位置(反应)方面具有一些智能,并会自动为您跟踪它。当可观察量 发生变化时,所有反应都会重新运行。有趣的是,这些反应可以是任何东西,从简单的控制台日志、网络调用到重新呈现 UI
flutter_redux
说到Redux 前端小伙伴一定都不陌生,Redux在React/VUE中与在Flutter/Dart中概念一样,只是使用上的不同。
它主要由三部分组成:
- Store: 它是整个数据的仓库,存储State对象,管理着整个应用的状态
- Reducer:处理与分发事件的方法,通过返回新的State来更新Store
- Action: 行为(也可以理解为事件),action将会分发至对应的reducer中
get
GetX 是一个超轻且强大的 Flutter 解决方案。它结合了高性能状态管理、智能依赖注入和路由管理,快速实用。GetX 有 3 个基本原则:
- 性能:GetX 专注于性能和最少的资源消耗。GetX 不使用 Streams 或 ChangeNotifier。
- 生产力:GetX 使用简单而愉快的语法。无论您想做什么,GetX 总有更简单的方法。它将节省开发时间,并提供您的应用程序可以提供的最大性能。
- 组织: GetX 允许视图、表示逻辑、业务逻辑、依赖注入和导航的完全解耦。
状态 (State) 管理参考
总结
Flutter/Dart内置支持setState,在不需要组件之间数据共享的情况下复杂度较低的组件内部使用还是不错的选择,使用简单方便。而provider基于InheritedWidget实现,简洁易用,也是Flutter官方推荐的状态管理框架,选择状态管理框架,应该从程序解耦的原理、库的设计、实现、代码量等因素来选择合适的状态管理。