今天让我们来了解一下各种Flutter状态管理库的内部机制。
我们把常见库按照如何传递状态
来分类:
- 用 widget (
StatefulWidget
,InheritedWidget
,provider
) - 用 stream (
flutter_bloc
,flutter_redux
) - 用 subscription (
getx
,riverpod
,flutter_mobx
) - 用 graph (
creator
)
用 widget
StatefulWidget
和 InheritedWidget
是 Flutter 的自带方案。
StatefulWidget
在一个 Widget 里存储状态,可以用回调函数来传递状态。
InheritedWidget
在一个子树的根节点存储状态。当状态改变时,对应的孩子节点会重建(DartPad)。
provider
对 InheritedWidget
添加了一些语法糖和lazy loading。 (DartPad) (code)
用 stream
很多人不喜欢业务逻辑跟UI逻辑混在一起,这时候可以考虑用Stream传递状态。
我们自己先写一个基本版本 (DartPad):
// counter.dart
int _count = 0;
final _controller = StreamController<int>..add(_count);
Stream<int> counter() => _controller.stream;
void increment() => _controller.add(_count++);
void decrement() => _controller.add(_count--);
// main.dart...
StreamBuilder<int>(
stream: counter(),
builder: (context, snapshot) => Text('${snapshot.data}'),
);
...
TextButton(onPressed: () => increment(), child: const Text('+1'));
TextButton(onPressed: () => decrement(), child: const Text('-1'));
然后把StreamController
放在一个class里(叫CounterCubit
或者 CounterBloc
或者 CounterStore
), 用 provide 提供这个class, 然后把 StreamBuilder
也放到一个class里 (叫BlocBuilder
或者 StoreBuilder
)。见代码 DartPad。
好啦,我们完成了 flutter_bloc
和 flutter_redux
的核心逻辑。
当然,这两个库还对状态改变函数做了一些限制,这其实属于design pattern而不是状态管理的范畴:
Cubit
(flutter_bloc) 允许自定义函数来改变状态。Bloc
/Store
需要你定义一些Event/Action然后通过处理他们来改变状态。
源代码: flutter_bloc (state, provider, stream), flutter_redux (state, InheritedWidget, stream).
用 subscription
还有一种方案,就是用经典的publisher/subscriber,监听者模式。这是一种响应式的编程方式。
简而言之,就是需要定义一些节点类,既能够存储状态,又能够跟其他节点建立依赖关系。他们组成了一张图:
同样的, 我们还是先自己实现一下 DartPad:
/// A stack tracks Rx variables being recreated.
List<Rx> _creating = [];
/// Basic reactive variable.
class Rx<T> {
Rx(this.create) {
_creating.add(this);
_state = create();
_creating.removeLast();
}
late T _state;
final T Function() create; // User provided create function
final Set<Rx> listeners = {};
T call() {
listeners.add(_creating.last);
return _state;
}
/// Update state with [update] function or [create] function.
void update([T Function(T)? update]) {
_creating.add(this);
final oldState = _state;
_state = update != null ? update(_state) : create();
_creating.removeLast();
if (oldState != _state) {
for (var l in listeners) {
l.update();
}
}
}
}
Rx num1 = Rx(() => 1);
Rx num2 = Rx(() => 2);
Rx sum = Rx(() => num1() + num2());
Rx widget = Rx(() => Text('${sum()}'));
实际上,这个节点可以叫 Provider
(riverpod
), Observable
(flutter_mobx
), 或者 Rx
(getx
):
// riverpod
Provider num1 = Provider((ref) => 1);
Provider num2 = Provider((ref) => 2);
Provider sum = Provider((ref) => ref.watch(num1) + ref.watch(num2));
Provider widget = Provider((ref) => Text('${ref.watch(sum)}');
// flutter_mobx
Observable num1 = Observable(0);
Observable num2 = Observable(0);
Computed sum = Computed(() => num1.value + num2.value);
Computed widget = Computed(() => Text('${sum.value}'));
// getx
Rx num1 = 1.obx;
Rx num2 = 2.obx;
int sum = num1.value + num2.value;
Obx widget = Obx(() => Text('${sum.value}'));
用这种模式最大的好处就是能够非常轻松的处理状态的衍生和组合逻辑。当前端状态复杂的时候很有用。
这几个库的内部实现都有些复杂,我们简单来看一下:
riverpod
它的逻辑跟我们上面自己写的代码很像。它跟其他两个库的最大区别是需要显示的建立依赖关系。我认为这其实是它的优点:依赖关系很好理解,而且不会不小心建立不必要的依赖。
它的代码复杂主要是实现了好多种不同的provider。
mobx
mobx跟riverpod的实现逻辑很像,不过把节点分成 Observable
和 Computed
。其中只有 Computed 需要维护自己的依赖关系。
mobx还提倡一些设计模式,认为改变状态要通过Action和Store。这一点让它不是特别灵活,并需要依赖代码生成。
getx
getx 自己就有好几种状态管理方案,其中响应式的最为复杂,我门这里只讨论它。
但其实getx不是完全响应式的,它不支持衍生的状态。仔细看上面的代码,sum
是一个integer,不是Rx
变量。如果两个widget都依赖sum
,运算会进行两次。
它的Rx
变量内部使用Stream
来传递状态,这一点也没有riverpod
和 flutter_mobx
自定义的响应式方案高效。
用 graph
Creator用图来传递状态是一种新的实现,也是一种简洁优美的方案。它跟上一节响应式的方案基本是一样的,只不过把图的关系更加显示的表达。这样做有两点好处:
- 更容易实现
async
的业务逻辑。 - 内部代码更加简洁清晰。
实现起来只需要定义好两个类,一个用来表示节点,另一个用来表示图。具体可以看这篇文章,100行代码自建状态管理库。
这个库最大的优点是简洁易学。它只有两种creator:
Creator
生成一系列的T
Emiter
生成一系列的Future<T>
看一个例子 (DartPad):
// Simple creators bind to UI.
final cityCreator = Creator.value('London');
final unitCreator = Creator.value('Fahrenheit');
// Write fluid code with methods like map, where, etc.
final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);
// Combine creators for business logic.
final temperatureCreator = Emitter<String>((ref, emit) async {
final f = await ref.watch(fahrenheitCreator);
final unit = ref.watch(unitCreator);
emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
});
总结
最后总结一下:
- 当理解了这些内部机制之后,状态管理其实很简单,完全可以自己挑选喜欢的元素组建一个自己的方案。
- 如果没有特殊的偏好,请尝试最新最简洁最优美的 Creator 库。
- 尽量不要用
getx
,它的响应式实现非常一般,更不要提它糟糕的测试覆盖率。