状态管理
Flutter 应用的 状态 指的是它用于显示界面或管理系统资源的所有对象。状态管理是我们组织应用以最有效方式访问这些对象并在不同 widget 之间共享它们的方式。
本页探讨了状态管理的许多方面,包括
- 使用
StatefulWidget
- 使用构造函数、
InheritedWidget
和回调在 widget 之间共享状态 - 使用
Listenable
在某些内容更改时通知其他 widget - 将 Model-View-ViewModel (MVVM) 用于应用架构
有关状态管理的其他介绍,请查看以下资源
- 视频:在 Flutter 中管理状态。本视频展示了如何使用 riverpod package。
flutter_dash 教程:状态管理。这展示了如何将 ChangeNotifer
与 provider package 结合使用。
本指南不使用 provider
或 Riverpod
等第三方 package。相反,它只使用 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 之间共享状态
#应用需要存储状态的一些场景包括以下方面
- 更新共享状态并通知应用的其他部分
- 监听共享状态的更改并在更改时重建界面
本节探讨了如何在应用中有效共享不同 widget 之间的状态。最常见的模式是
- 使用 widget 构造函数(在其他框架中有时称为“prop drilling”)
- 使用
InheritedWidget
(或类似 API,例如 provider package)。 - 使用回调通知父 widget 某些内容已更改
使用 widget 构造函数
#由于 Dart 对象是按引用传递的,因此 widget 通常会在其构造函数中定义它们需要使用的对象。您传递给 widget 构造函数的任何状态都可以用于构建其界面
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()
。在 build
方法中调用 of()
的 widget 会创建一个由 Flutter 框架管理的依赖项,因此当此 widget 使用新数据重新构建并且 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
使用可监听对象
#既然您已经选择了如何在应用中共享状态,那么当状态更改时,您如何更新界面呢?您如何以通知应用其他部分的方式更改共享状态?
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 请求、缓存数据或管理插件等系统资源。Model 通常不需要导入 Flutter 库。
例如,考虑一个使用 HTTP 客户端加载或更新计数器状态的 model
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 {
// ...
}
}
此 model 不使用任何 Flutter 原语,也不对其运行的平台做任何假设;它的唯一工作是使用其 HTTP 客户端获取或更新计数。这使得 model 可以在单元测试中通过 Mock 或 Fake 实现,并定义了应用低级组件与构建完整应用所需的高级 UI 组件之间的清晰边界。
CounterData
类定义了数据的结构,并且是应用的真正“model”。Model 层通常负责应用所需的核心算法和数据结构。如果您对定义 model 的其他方式感兴趣,例如使用不可变值类型,请查看 pub.dev 上的 freezed
或 built_collection
等 package。
定义 ViewModel
#ViewModel
将 View 绑定到 Model。它保护 model 不被 View 直接访问,并确保数据流从 model 的更改开始。数据流由 ViewModel
处理,它使用 notifyListeners
通知 View 某些内容已更改。ViewModel
就像餐厅里的服务员,负责处理厨房(model)和顾客(views)之间的沟通。
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
从 Model 收到错误时,它会存储一个 errorMessage
。这可以保护 View 免受未处理的运行时错误(可能导致崩溃)。相反,View 可以使用 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'),
),
],
);
},
)
这种模式允许应用的业务逻辑与界面逻辑以及 Model 层执行的低级操作分离。
了解更多状态管理信息
#本页仅触及了状态管理的皮毛,因为组织和管理 Flutter 应用状态的方法有很多种。如果您想了解更多信息,请查看以下资源
- 文章:状态管理方法列表
- 存储库:Flutter 架构示例
反馈
#由于本网站的此部分正在不断完善中,我们欢迎您的反馈!