Flutter 极简状态管理:基于 ValueNotifier 实现
在 Flutter 开发中,状态管理方案的选择往往让开发者感到困惑。从 Stacked、Provider、Bloc 到全能的 GetX,从 web 端过来的 Redux、Mobx、Signal,再到最近很火的 Provider 的亲兄弟 Riverpod,各种方案都有其独特的优势和适用场景,但每一种都有一些地方让我难以理解或认同,要么规定了很多骚操作,要么模板 代码太多,要么背后隐藏着太多 “魔法”,而我需要的是简单易懂、尽可能简洁、可扩展的方案。
于是便有了这篇文章,我要介绍一种不使用第三方状态管理方案的方法,它唯一用到的第三方包是 GetIt(但不是用于管理状态,而是依赖注入管理),而对于状态改变时重建 UI,会使用 Flutter 自带的 ValueNotifier
和 ValueListenableBuilder
类。
为什么不使用第三方状态管理方案?
我不是劝大家放弃正在使用的第三方状态管理方案,如果当前用着顺手就没必要改变。 我在不同的项目里也会选择不同的第三方状态管理方案,比如 Provider、GetX、Riverpod,我都有在项目中实际用过,不是说他们不好,这取决于项目的需求和个人的偏好。
我有的时候就不想选择第三方状态管理方案,主要考虑了以下几个主要原因:
- 复杂性与学习成本
- 第三方状态管理库通常具有复杂的概念和使用方式。例如,一些库引入了大量的抽象概念和设计模式,如 Redux 的单向数据流和多个中间件的概念,这对于初学者来说理解和掌握起来较为困难。
- 学习和使用这些库需要花费大量的时间和精力去理解其内部机制、各种概念之间的关系以及如何正确地应用它们。这可能会导致开发周期延长,尤其是在项目初期需要快速搭建原型和实现基本功能时。
- 项目的依赖管理
- 引入第三方库会增加项目的依赖关系。不同的第三方库可能存在版本兼容性问题,当需要更新某个库时,可能会引发一系列的依赖冲突,需要花费额外的时间和精力去解决。
- 过多的依赖也会增加应用的体积和构建时间,这对于移动应用来说是需要考虑的重要因素,尤其是在应用需要在资源有限的设备上运行时。
setState
不够用吗?
Flutter 本身提供了一些基本的状态管理方式,如setState
方法,对于简单的应用场景,开发者可以快速上手,通过在StatefulWidget
中调用setState
来更新 UI,这种方式直观且易于理解。
但是,对于复杂的应用场景,setState
无法满足需求。例如,当应用具有多个页面和复杂的状态交互时,使用setState
可能会导致代码结构混乱,难以维护。
随着应用的不断发展和功能的增加,setState
无法很好地适应新的需求,缺乏足够的扩展性和灵活性来处理复杂的业务逻辑和状态转换。
最重要的是每次调用 setState
都会触发 UI 的重建,这可能会导致性能问题,尤其是在复杂的 UI 结构中。
所以,setState
通常用于更新局部状态,而不是全局状态。并且setState
通常与 UI 逻辑耦合,难以进行单元测试。
为什么选择 ValueNotifier
?
ValueNotifier
是一种特殊的观察者模式实现。它内部维护一个状态值,并提供了一种机制,当这个状态值发生变化时,能够通知所有注册的观察者。在 Flutter 中,UI 组件可以作为观察者,通过ValueListenableBuilder
来监听ValueNotifier
的状态变化。
例如,当一个 ValueNotifier 的状态值从1
变为2
时,所有与该 ValueNotifier 关联的ValueListenableBuilder
都会收到通知,进而触发相关 UI 组件的重建。
在介绍具体实现之前,让我们先了解为什么要选择 ValueNotifier
,它有什么优势:
-
状态隔离与精确更新
ValueNotifier
能够很好地隔离应用中的状态,使得每个状态都能被独立管理。例如,在一个具有多个数据展示区域的应用中,不同的状态可以使用各自的ValueNotifier
进行管理。当某个状态发生变化时,只有与该状态相关的 UI 部分会收到通知并更新,避免了不必要的 UI 重建,提高了应用的性能和响应速度。- 与其他可能导致大量 UI 重建的状态管理方式相比,
ValueNotifier
更加精确地控制了状态变化对 UI 的影响。例如,使用ChangeNotifier
时,可能会因为状态的一处更新而导致过多的 UI 组件重建,即使这些组件与更新的状态并无直接关联。
-
支持不可变状态
ValueNotifier
支持不可变状态的管理,这符合现代编程中对数据一致性和可预测性的要求。在处理复杂的业务逻辑和多线程环境下,不可变状态能够减少因数据意外修改而导致的错误,提高应用的稳定性。 -
与 Flutter 框架的集成
ValueNotifier
是 Flutter 框架自带的组件,无需引入额外的第三方库,这使得应用的依赖关系更加简单,减少了因第三方库版本冲突或兼容性问题带来的风险。
相比其他方案:
- Provider:需要额外依赖,使用
Context
和InheritedWidget
。 - GetX:功能强大但概念较多,存在一定学习成本。
- Bloc:适合大型应用,但对于简单场景可能过于复杂。
- Riverpod:功能强大,但是学习曲线较陡峭。
如何基于ValueNotifier
实现状态管理?
一个良好的状态管理解决方案需要具备以下能力:
- 为 UI 层提供指向状态管理层的引用
- 把 UI 事件通知给状态管理层
- 给 UI 层提供一种监听状态变化的方式
- 在状态变化后重建 UI
下面我们逐个来看这些方面。
为 UI 层提供对状态管理层的引用
- 在有状态组件中创建实例
我们可以在应用的某个页面组件中,如
MyPage
,在其有状态组件_MyPageState
中创建状态管理类的实例。
class MyPage extends StatefulWidget {
//...
class _MyPageState extends State<MyPage> {
final manager = MyPageManager();
//...
- 使用 GetIt 获取引用
另一种方式是使用
GetIt
来获取状态管理类的引用。在MyPage
组件的build
方法中可以这样获取:
class MyPage extends StatelessWidget {
Widget build(BuildContext context) {
final manager = getIt<MyPageManager>();
// return...
}
使用GetIt
时需要注意遵循不在 UI 层直接修改状态的原则,而是通过调用状态管理类的方法来进行状态的更新。
通知状态管理层关于 UI 事件
当用户在 UI 层进行操作时,例如点击按钮,UI 层需要通知状态管理层。假设点击按钮的处理方法在状态管理类MyPageManager
中定义为handleButtonClick
,在 UI 层的按钮点击事件处理中可以这样调用:
onPressed: () {
final stateManager = getIt<MyPageManager>();
stateManager.handleButtonClick();
},
如果页面打开时需要执行一些初始化逻辑,比如加载初始数据,可以在有状态组件的initState
方法中进行处理:
void initState() {
super.initState();
manager.loadInitialData();
}
为 UI 层提供监听状态变化的方法
在状态管理类中,我们使用 ValueNotifier
来处理状态的变化。例如,对于某个计数器状态,我们可
以创建一个 ValueNotifier:
final counterNotifier = ValueNotifier<int>(0);
当计数器的值发生变化时,可以更新 ValueNotifier
的值:
counterNotifier.value = newCounterValue;
对于更复杂的状态,比如一个包含多个字段的用户信息对象,我们可以创建一个扩展 ValueNotifier
的类。假设我们有一个UserInfo
类来表示用户信息,我们可以创建UserInfoNotifier
如下:
class UserInfoNotifier extends ValueNotifier<UserInfo> {
UserInfoNotifier() : super(UserInfo());
//在实际应用中,此处可能需要获取服务层的引用,用于更新用户信息
// final _userService = getIt<UserService>();
void updateUserInfo() {
//在实际应用中,此处可能需要从服务层获取新的用户信息并更新
// final newUserInfo = _userService.getUpdatedUserInfo();
value = newUserInfo;
}
这样,在状态管理类MyPageManager
中,我们可以使用这些 ValueNotifier
来管理状态。
在状态变化后重建 UI
UI 层通过 ValueListenableBuilder
来监听状态的变化并重建相应的组件。例如,对于显示计数器值的 Text
组件,我们可以这样使用 ValueListenableBuilder
:
class CounterWidget extends StatelessWidget {
const CounterWidget({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final myPage = getIt<MyPageManager>();
return ValueListenableBuilder<int>(
valueListenable: myPage.counterNotifier,
builder: (context, value, child) {
return Text(
'$value',
style: Theme.of(context).textTheme.headline2,
//如果此处的组件有子组件且不受状态变化影响,可以通过child参数传递
// child: UnchangedChildWidget(),
);
},
);
}
当计数器的值发生变化时,ValueListenableBuilder
会自动重建 Text
组件,显示新的计数器值。
PS:有时候用ValueListenableBuilder
包裹部件挺麻烦的,不过如果在 VS Code 或者 Android Studio 中使用快捷键调出上下文菜单,可以选择Wrap with StreamBuilder,然后再把StreamBuilder
修改为ValueListenableBuilder
就行。
我推荐保持简单,所有情况都使用ValueNotifier
和ValueListenableBuilder
,不过如果决定暴露Stream
、Future
或者ChangeNotifier
,也有对应的构建器部件:
- Stream:使用StreamBuilder。
- Future:使用FutureBuilder。
- ChangeNotifier:使用ListenableBuilder。
但如果追求极简和简单,其实不需要StreamBuilder
、FutureBuilder
或者ChangeNotifier
,所有情况都用ValueNotifier
和ValueListenableBuilder
就行,在状态管理类里处理Future
和Stream
相关的内容。