状态管理
Flutter 应用的*状态*指的是它用于显示 UI 或管理系统资源的所有对象。状态管理是我们组织应用的方式,以便最有效地访问这些对象并在不同 widget 之间共享它们。
本页面探讨了状态管理的许多方面,包括:
- 使用
StatefulWidget
- 使用构造函数、
InheritedWidget
和回调在 widget 之间共享状态 - 使用
Listenable
在有东西改变时通知其他 widget - 为你的应用架构使用 Model-View-ViewModel (MVVM)
有关状态管理的其他介绍,请查看这些资源:
- 视频:在 Flutter 中管理状态。此视频展示了如何使用 riverpod 包。
flutter_dash 教程:状态管理。此教程展示了如何将 ChangeNotifier
与 provider 包一起使用。
本指南不使用 provider 或 Riverpod 等第三方包。相反,它只使用 Flutter 框架中提供的原生功能。
使用 StatefulWidget
#管理状态最简单的方法是使用 StatefulWidget
,它将状态存储在自身内部。例如,考虑以下 widget:
class MyCounter extends StatefulWidget {
const MyCounter({super.key});
@override
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
TextButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
)
],
);
}
}
此代码说明了状态管理中两个重要的概念:
- 封装:使用
MyCounter
的 widget 无法看到底层的count
变量,也无法访问或更改它。 - 对象生命周期:
_MyCounterState
对象及其count
变量在MyCounter
首次构建时创建,并一直存在直到它从屏幕上移除。这是*瞬时状态*的一个例子。
你可能会发现以下资源很有用:
- 文章:瞬时状态和应用状态
- API 文档:StatefulWidget
在 widget 之间共享状态
#应用需要存储状态的一些场景包括:
- 更新共享状态并通知应用的其他部分
- 监听共享状态的变化并在变化时重建 UI
本节探讨了如何有效地在应用的不同 widget 之间共享状态。最常见的模式是:
- 使用 widget 构造函数(在其他框架中有时称为“prop drilling”)。
- 使用
InheritedWidget
(或类似的 API,例如 provider 包)。 - 使用回调函数通知父 widget 有东西改变了
使用 widget 构造函数
#由于 Dart 对象是按引用传递的,因此 widget 在其构造函数中定义它们需要使用的对象是非常常见的。你传递给 widget 构造函数的任何状态都可以用来构建其 UI。
class MyCounter extends StatelessWidget {
final int count;
const MyCounter({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
这使得你的 widget 的其他用户清楚地知道他们需要提供什么才能使用它。
Column(
children: [
MyCounter(
count: count,
),
MyCounter(
count: count,
),
TextButton(
child: Text('Increment'),
onPressed: () {
setState(() {
count++;
});
},
)
],
)
通过 widget 构造函数在应用中传递共享数据,可以清楚地向任何阅读代码的人表明存在共享依赖。这是一种常见的设计模式,称为*依赖注入*,许多框架都利用它或提供工具来使其更易于使用。
使用 InheritedWidget
#手动向下传递数据到 widget 树可能冗长并导致不必要的样板代码,因此 Flutter 提供了 *InheritedWidget
*,它提供了一种在父 widget 中高效托管数据的方式,以便子 widget 可以访问它们而无需将它们作为字段存储。
要使用 InheritedWidget
,请扩展 InheritedWidget
类并使用 dependOnInheritedWidgetOfExactType
实现静态方法 of()
。在构建方法中调用 of()
的 widget 会创建一个由 Flutter 框架管理的依赖关系,因此当此 InheritedWidget
使用新数据重新构建并且 updateShouldNotify
返回 true 时,任何依赖于此 InheritedWidget
的 widget 都会重新构建。
class MyState extends InheritedWidget {
const MyState({
super.key,
required this.data,
required super.child,
});
final String data;
static MyState of(BuildContext context) {
// This method looks for the nearest `MyState` widget ancestor.
final result = context.dependOnInheritedWidgetOfExactType<MyState>();
assert(result != null, 'No MyState found in context');
return result!;
}
@override
// This method should return true if the old widget's data is different
// from this widget's data. If true, any widgets that depend on this widget
// by calling `of()` will be re-built.
bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}
接下来,在需要访问共享状态的 widget 的 build()
方法中调用 of()
方法。
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
var data = MyState.of(context).data;
return Scaffold(
body: Center(
child: Text(data),
),
);
}
}
使用回调函数
#你可以通过公开回调函数来通知其他 widget 值发生了变化。Flutter 提供了 ValueChanged
类型,它声明了一个带有一个参数的函数回调。
typedef ValueChanged<T> = void Function(T value);
通过在 widget 的构造函数中公开 onChanged
,你为使用此 widget 的任何 widget 提供了一种在你的 widget 调用 onChanged
时进行响应的方式。
class MyCounter extends StatefulWidget {
const MyCounter({super.key, required this.onChanged});
final ValueChanged<int> onChanged;
@override
State<MyCounter> createState() => _MyCounterState();
}
例如,此 widget 可能会处理 onPressed
回调,并使用其最新的内部 count
变量状态调用 onChanged
。
TextButton(
onPressed: () {
widget.onChanged(count++);
},
),
深入探究
#有关在 widget 之间共享状态的更多信息,请查看以下资源:
- 文章:Flutter 架构概述——状态管理
- 视频:实用状态管理
- 视频:InheritedWidgets
- 视频:Inherited Widgets 指南
- 示例:Provider 购物者
- 示例:Provider 计数器
- API 文档:
InheritedWidget
使用可监听对象
#现在你已经选择了如何在应用中共享状态,那么当状态改变时如何更新 UI 呢?你如何以一种通知应用其他部分的方式改变共享状态呢?
Flutter 提供了一个名为 Listenable
的抽象类,它可以更新一个或多个监听器。使用可监听对象的一些有用方式是:
- 使用
ChangeNotifier
并使用ListenableBuilder
订阅它 - 使用
ValueNotifier
和ValueListenableBuilder
ChangeNotifier
#要使用 ChangeNotifier
,创建一个继承它的类,并在类需要通知其监听器时调用 notifyListeners
。
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
然后将其传递给 ListenableBuilder
,以确保每当 ChangeNotifier
更新其监听器时,由 builder
函数返回的子树都会重新构建。
Column(
children: [
ListenableBuilder(
listenable: counterNotifier,
builder: (context, child) {
return Text('counter: ${counterNotifier.count}');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.increment();
},
),
],
)
ValueNotifier
#一个 ValueNotifier
是 ChangeNotifier
的一个更简单的版本,它存储一个单一的值。它实现了 ValueListenable
和 Listenable
接口,因此它与 ListenableBuilder
和 ValueListenableBuilder
等 widget 兼容。要使用它,请创建 ValueNotifier
的实例并提供初始值。
ValueNotifier<int> counterNotifier = ValueNotifier(0);
然后使用 value
字段读取或更新值,并通知任何监听器值已更改。因为 ValueNotifier
扩展了 ChangeNotifier
,它也是一个 Listenable
,可以与 ListenableBuilder
一起使用。但你也可以使用 ValueListenableBuilder
,它在 builder
回调中提供了值。
Column(
children: [
ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return Text('counter: $value');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.value++;
},
),
],
)
深入探究
#要了解更多关于 Listenable
对象的信息,请查看以下资源:
- API 文档:
Listenable
- API 文档:
ValueNotifier
- API 文档:
ValueListenable
- API 文档:
ChangeNotifier
- API 文档:
ListenableBuilder
- API 文档:
ValueListenableBuilder
- API 文档:
InheritedNotifier
为你的应用架构使用 MVVM
#现在我们了解了如何共享状态以及当状态改变时如何通知应用的其他部分,我们准备开始思考如何组织应用中的有状态对象。
本节描述了如何实现一种与 Flutter 等响应式框架配合良好的设计模式,称为 *Model-View-ViewModel* 或 *MVVM*。
定义模型 (Model)
#Model 通常是一个 Dart 类,执行低级任务,例如发出 HTTP 请求、缓存数据或管理插件等系统资源。模型通常不需要导入 Flutter 库。
例如,考虑一个使用 HTTP 客户端加载或更新计数器状态的模型:
import 'package:http/http.dart';
class CounterData {
CounterData(this.count);
final int count;
}
class CounterModel {
Future<CounterData> loadCountFromServer() async {
final uri = Uri.parse('https://myfluttercounterapp.net/count');
final response = await get(uri);
if (response.statusCode != 200) {
throw ('Failed to update resource');
}
return CounterData(int.parse(response.body));
}
Future<CounterData> updateCountOnServer(int newCount) async {
// ...
}
}
这个模型不使用任何 Flutter 原语,也不对其运行的平台做任何假设;它唯一的工作是使用其 HTTP 客户端获取或更新计数。这允许模型在单元测试中使用 Mock 或 Fake 实现,并定义了应用低级组件和构建完整应用所需的高级 UI 组件之间的清晰界限。
CounterData
类定义了数据的结构,是应用真正的“模型”。模型层通常负责应用所需的核心算法和数据结构。如果你对定义模型的其他方式感兴趣,例如使用不可变值类型,请查看 pub.dev 上的 freezed 或 build_collection 等包。
定义视图模型 (ViewModel)
#ViewModel
将*视图*绑定到*模型*。它保护模型不被视图直接访问,并确保数据流从模型的更改开始。数据流由 ViewModel
处理,它使用 notifyListeners
通知视图有东西改变了。ViewModel
就像餐厅里的服务员,处理厨房(模型)和顾客(视图)之间的沟通。
import 'package:flutter/foundation.dart';
class CounterViewModel extends ChangeNotifier {
final CounterModel model;
int? count;
String? errorMessage;
CounterViewModel(this.model);
Future<void> init() async {
try {
count = (await model.loadCountFromServer()).count;
} catch (e) {
errorMessage = 'Could not initialize counter';
}
notifyListeners();
}
Future<void> increment() async {
final currentCount = count;
if (currentCount == null) {
throw('Not initialized');
}
try {
final incrementedCount = currentCount + 1;
await model.updateCountOnServer(incrementedCount);
count = incrementedCount;
} catch(e) {
errorMessage = 'Could not update count';
}
notifyListeners();
}
}
请注意,当 ViewModel
从模型接收到错误时,它会存储一个 errorMessage
。这可以保护视图免受未处理的运行时错误的影响,这些错误可能导致崩溃。相反,视图可以使用 errorMessage
字段来显示用户友好的错误消息。
定义视图 (View)
#由于我们的 ViewModel
是一个 ChangeNotifier
,任何引用它的 widget 都可以使用 ListenableBuilder
来在 ViewModel
通知其监听器时重建其 widget 树。
ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Column(
children: [
if (viewModel.errorMessage != null)
Text(
'Error: ${viewModel.errorMessage}',
style: Theme.of(context)
.textTheme
.labelSmall
?.apply(color: Colors.red),
),
Text('Count: ${viewModel.count}'),
TextButton(
onPressed: () {
viewModel.increment();
},
child: Text('Increment'),
),
],
);
},
)
这种模式允许你的应用的业务逻辑与 UI 逻辑和模型层执行的低级操作分离。
了解更多关于状态管理
#本页面仅触及状态管理的皮毛,因为组织和管理 Flutter 应用状态的方法有很多。如果你想了解更多,请查看以下资源:
- 文章:状态管理方法的列表
- 仓库:Flutter 架构示例
反馈
#由于本网站的这一部分正在不断发展,我们欢迎你的反馈!