iOS 和 Android 的原生开发模式是命令式编程模式。命令式编程要求开发者一步步描述整个构建过程,从而引导程序去构建用户界面。
Flutter 则采用了声明式编程模式,框架隐藏了具体的构建过程,开发者只需要声明状态,框架会自动构建用户界面。这也就意味着 Flutter 构建的用户界面就是当前的状态。
App 在运行中总是会更新用户界面,因此我们需要对状态进行有效的管理。状态管理本质上就是 如何解决状态读/写的问题 。对此,我们将从两个方面去评估状态管理方案:
此外,根据 Flutter 原生支持的情况,我们将 Flutter 状态管理方案分为两类:
下文,我们将以 Flutter 官方的计数器例子来介绍 Flutter 中的状态管理方案,并逐步进行优化。
关于本文涉及的源码,见 【Demo 传送门】 。
Flutter 模板工程就是【直接访问 + 直接更新】的状态管理方案。这种方案的状态访问/更新示意图如下所示。
很显然,【直接访问 + 直接更新】方案只适合于在单个 StatefulWidget
中进行状态管理。那么对于多层级的 Widget 结构该如何进行状态管理呢?
对于多层级的 Widget 结构,状态是无法直接访问和更新的。因为 Widget 和 State 是分离的,并且 State 一般都是私有的,所以子 Widget 是无法直接访问/更新父 Widget 的 State。
对于这种情况,最直观的状态管理方案就是:【状态传递 + 闭包传递】。对于状态访问,父 Widget 在创建子 Widget 时就将状态传递给子 Widget;对于状态更新,父 Widget 将更新状态的操作封装在闭包中,传递给子 Widget。
这里存在一个问题:当 Widget 树层级比较深时,如果中间有些 Widget 并不需要访问或更新父 Widget 的状态时,这些中间 Widget 仍然需要进行辅助传递。很显然,这种方案在 Widget 树层级较深时,效率比较低,只适合于较浅的 Widget 树层级。
那么如何优化多层级 Widget 树结构下的状态管理方案呢?我们首先从状态更新方面进行优化。
【状态传递 + Notification】方案采用 Notification 定向地优化了状态更新的方式。
通知(Notification)是 Flutter 中一个重要的机制,在 Widget 树种,每个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过 NotificationListener
来监听通知。Flutter 中将这种由子向父的传递通知的机制称为 通知冒泡 (Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同: 通知冒泡可以中止,而用户触摸事件无法中止 。
下图所示为这种方案的状态访问/更新示意图。
具体的实现源码如下所示:
// 与 父 Widget 绑定的 State class _PassStateNotificationDemoPageState extends State{ int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { // 父 Widget 使用 NotificationListener 监听通知 return NotificationListener ( onNotification: (notification) { setState(() { _incrementCounter(); }); return true; // true: 阻止冒泡;false: 继续冒泡 }, child: Scaffold( ... ), ); } } /// 子 Widget class _IncrementButton extends StatelessWidget { int counter = 0; _IncrementButton(this.counter); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => IncrementNotification("加一操作").dispatch(context), // 点击按钮触发通知派发 child: ...) ); } } /// 自定义通知 class IncrementNotification extends Notification { final String msg; IncrementNotification(this.msg); }
【传递传递 + Notification】方案定向优化了状态的更新,那么如何进一步优化状态的访问呢?
【InheritedWidget + Notification】方案采用 InhertiedWidget
实现了在多层级 Widget 树中直接访问状态的能力。
InheritedWidget
是 Flutter 中非常重要的一个功能型组件,其提供了一种数据在 Widget 树中从上到下传递、共享的方式。这与 Notification 的传递方向正好相反。我们在父 Widget 中通过 InheritedWidget
共享一个数据,那么任意子 Widget 都能够直接获取到共享的数据。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
/// 与父 Widget 绑定的 State class _InheritedWidgetNotificationDemoPageState extends State{ int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return CounterInheritedWidget( counter: _counter, child: NotificationListener ( onNotification: (notification) { setState(() { _incrementCounter(); }); return true; // true: 阻止冒泡;false: 继续冒泡 }, child: Scaffold( ... ), ), ), ), ); } } /// 子 Widget class _IncrementButton extends StatelessWidget { _IncrementButton(); @override Widget build(BuildContext context) { // 直接获取状态 final counter = CounterInheritedWidget.of(context).counter; return GestureDetector( onTap: () => IncrementNotification("加一").dispatch(context), // 派发通知 child: ... ); } } /// 对使用自定义的 InheritedWidget 子类对状态进行封装 class CounterInheritedWidget extends InheritedWidget { final int counter; // 需要在子树中共享的数据,保存点击次数 CounterInheritedWidget({@required this.counter, Widget child}) : super(child: child); // 定义一个便捷方法,方便子树中的widget获取共享数据 static CounterInheritedWidget of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType (); } @override bool updateShouldNotify(CounterInheritedWidget old) { // 如果返回true,则子树中依赖(build函数中有调用)本widget // 的子widget的`state.didChangeDependencies`会被调用 return old.counter != counter; } }
虽然【InheritedWidget + Notification】方案在状态访问和状态更新方面都进行了优化,但是从其状态管理示意图上看,状态的更新仍然具有优化空间。
【InheritedWidget + EventBus】方案则采用了 事件总线 (Event Bus)的方式管理状态更新。
事件总线是 Flutter 中的一种全局广播机制,可以实现跨页面事件通知。事件总线通常是一种订阅者模式,其包含发布者和订阅者两种角色。
【InheritedWidget + EventBus】方案将子 Widget 作为发布者,父 Widget 作为订阅者。当子 Widget 进行状态更新时,则发出事件,父 Widget 监听到事件后进行状态更新。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
/// 与父 Widget 绑定的状态 class _InheritedWidgetEventBusDemoPageState extends State{ int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override void initState() { super.initState(); // 订阅事件 bus.on(EventBus.incrementEvent, (_) { _incrementCounter(); }); } @override void dispose() { // 取消订阅 bus.off(EventBus.incrementEvent); super.dispose(); } ... } /// 子 Widget class _IncrementButton extends StatelessWidget { _IncrementButton(); @override Widget build(BuildContext context) { final counter = CounterInheritedWidget.of(context).counter; return GestureDetector( onTap: () => bus.emit(EventBus.incrementEvent), // 发布事件 child: ... ); } }
两种方案的对比
【InheritedWidget + Notification】和【InheritedWidget + EventBus】的区别主要在于状态更新。两者对于状态的更新其实并没有达到最佳状态,都是通过一种间接的方式实现的。
相比而言,事件总线是基于全局,逻辑难以进行收敛,并且还要管理监听事件、取消订阅。从这方面而言,【InheritedWidget + Notification】方案更优。
从状态管理示意图而言,显然【InheritedWidget + Notification】还有进一步的优化空间。这里,我们可能会想:状态能否直接提供更新方法,当子 Widget 获取到状态后,直接调用状态的更新方法呢?
对此,官方推荐了一套基于第三方 Pub 的 Provider 状态管理方案。
【Provider】的本质是 基于 InheritedWidget
和 ChangeNotifier
进行了封装 。此外,使用缓存提升了性能,避免不必要的重绘。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
/// 与父 Widget 绑定的 State class _ProviderDemoPageState extends State{ @override Widget build(BuildContext context) { return ChangeNotifierProvider ( create: (_) => CounterProviderState(), // 创建状态 child: Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You have pushed the button this many times:', ), // 使用 provider 提供的 builder 使用状态 Consumer (builder: (context, counter, _) => Text("${counter.value}", style: Theme.of(context).textTheme.display1)), _IncrementButton(), ], ), ), ), ); } } /// 子 Widget class _IncrementButton extends StatelessWidget { _IncrementButton(); @override Widget build(BuildContext context) { // 访问状态 final _counter = Provider.of (context); return GestureDetector( onTap: () => _counter.incrementCounter(), // 更新状态 child: ... ); } } /// 自定义的状态,继承自 ChangeNotifier class CounterProviderState with ChangeNotifier { int _counter = 0; int get value => _counter; // 状态提供的更新方法 void incrementCounter() { _counter++; notifyListeners(); } }
Flutter 社区早期使用的 Scoped Model 方案与 Provider 的实现原理基本是一致的。
对于声明式(响应式)编程中的状态管理,Redux 是一种常见的状态管理方案。【Redux】方案的状态管理示意图与【Provider】方案基本上是一致的。
在这个基础上, Redux 对于状态更新的过程进行了进一步的细分和规划 ,使得其数据的流动过程如下所示。
一个 Store 存储多个状态,适合用于全局状态管理。
具体的实现源码如下所示。
/// 与父 Widget 绑定的 State class _ReduxDemoPageState extends State{ // 初始化 Store,该过程包括了对 State 的初始化 final store = Store (reducer, initialState: CounterReduxState.initState()); @override Widget build(BuildContext context) { return StoreProvider ( store: store, child: Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You have pushed the button this many times:', ), // 通过 StoreConnector 访问状态 StoreConnector ( converter: (store) => store.state.value, builder: (context, count) { return Text("$count", style: Theme.of(context).textTheme.display1); }, ), _IncrementButton(), ], ), ), ), ); } } /// 子 Widget class _IncrementButton extends StatelessWidget { _IncrementButton(); @override Widget build(BuildContext context) { return StoreConnector ( converter: (store) { return () => store.dispatch(Action.increment); // 发出 Action 以进行状态更新 }, builder: (context, callback) { return GestureDetector( onTap: callback, child: StoreConnector ( converter: (store) => store.state.value, builder: (context, count) { return ...; }, ) ); }, ); } } /// 自定义状态 class CounterReduxState { int _counter = 0; int get value => _counter; CounterReduxState(this._counter); CounterReduxState.initState() { _counter = 0; } } /// 自定义 Action enum Action{ increment } /// 自定义 Reducer CounterReduxState reducer(CounterReduxState state, dynamic action) { if (action == Action.increment) { return CounterReduxState(state.value + 1); } return state; }
【BLoC】方案是谷歌的两位工程师 Paolo Soares 和 Cong Hui 提出的一种状态管理方案,其状态管理示意图同样与【Provider】方案是一致的。
【BLoC】方案的底层实现与【Provider】是非常相似的,也是基于 InheritedWidget
进行状态访问,并且对状态进行了封装,从而提供直接更新状态的方法。
但是,BLoC 的核心思想是 基于流来管理数据 ,并且将业务逻辑均放在 BLoC 中进行,从而实现视图与业务的分离。
具体的实现源码如下所示。
/// 与父 Widget 绑定的 State class _BlocDemoPageState extends State{ // 创建状态 final bloc = CounterBloc(); @override Widget build(BuildContext context) { // 以 InheritedWidget 的方式提供直接方案 return BlocProvider( bloc: bloc, child: Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You have pushed the button this many times:', ), // 状态访问 StreamBuilder (stream: bloc.value, initialData: 0, builder: (BuildContext context, AsyncSnapshot snapshot) { return Text("${snapshot.data}", style: Theme.of(context).textTheme.display1); },), _IncrementButton(), ], ), ), ) ); } } /// 子 Widget class _IncrementButton extends StatelessWidget { _IncrementButton(); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => BlocProvider.of(context).increment(), // 状态更新 child: ClipOval(child: Container(width: 50, height: 50, alignment: Alignment.center,color: Colors.blue, child: StreamBuilder (stream: BlocProvider.of(context).value, initialData: 0, builder: (BuildContext context, AsyncSnapshot snapshot) { // 状态访问 return Text("${snapshot.data}", textAlign: TextAlign.center,style: TextStyle(fontSize: 24, color: Colors.white)); },),),) ); } } /// 自定义 BLoC Provider,继承自 InheritedWidget class BlocProvider extends InheritedWidget { final CounterBloc bloc; BlocProvider({this.bloc, Key key, Widget child}) : super(key: key, child: child); @override bool updateShouldNotify(_) => true; static CounterBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc; } /// 自定义的状态 class CounterBloc { int _counter; StreamController _counterController; CounterBloc() { _counter = 0; _counterController = StreamController .broadcast(); } Stream get value => _counterController.stream; increment() { _counterController.sink.add(++_counter); } dispose() { _counterController.close(); } }
一般而言,对于普通的项目来说【Provider】方案是一种非常容易理解,并且实用的状态管理方案。
对于大型的项目而言,【Redux】 有一套相对规范的状态更新流程,但是模板代码会比较多;对于重业务的项目而言,【BLoC】能够将复杂的业务内聚到 BLoC 模块中,实现业务分离。
总之,各种状态管理方案都有着各自的优缺点,这些需要我们在实践中去发现和总结,从而最终找到一种适合自己项目的状态管理方案。