2021-11-12

[翻译] 使用 Compose 进行状态管理的现状

原文链接: https://code.cash.app/the-state-of-managing-state-with-compose
作者:JakeWharton

Cash App 从五年前开始将 Android 客户端 UI 的 render 和 presenter 分离到不同类里。过去的几年里我们重度使用 RxJava,对这个过程帮助很大。我曾做过一个分享,使用 RxJava 进行状态管理的现状,在那里我重新定义了 RxJava 的(反)模式以适用于我们想要的架构。

尽管干净的分层改善了测试性,但是管理状态的代码变得不够清晰,难以理解。业务逻辑被淹没在海量的 RxJava 操作符的组合与嵌套之中。我们尝试过几种 redux 类的库,甚至还自己实现了一个,想避免这种情况,但效果都不够理想。

没多久,我离开了 Cash App 去了 Google,开发了 SdkSearch 继续测试类似的架构。在此期间从 RxJava 迁移到 kotlinx.coroutines 的 Channel 解锁了多平台支持。却没有迁移到 Flow,迁移本身并没有难度,问题不再这,而是生成状态的逻辑应该如何定义。我对我想要的方式有一个画面,但是不能避免陷入大量 API 之中,简单的表达出来。

回到 Cash App 之后,我仍然不认为现有 Flow 和协程的任何形式是一个足够好的解决方案。我开始尝试使用 Compose 去构建命令行 UI
多平台 UI binding,思考适用于所有基于 Compose 项目的架构应扮演的角色。 Matt Precious 今年早些时候做了一个 Compose Web 项目,我们基于此反复迭代一个典型的 presenter/render 分离在 Compose 中应该是什么样子的。我们搞了点好玩的东西,不过它依赖了 Compose,所以只能用于 Compose UI 和 Compose Web。

或者说,可以?

Enter Molecule

Molecule 基于一个想法,Compose 只用于产生状态,而不是去渲染的 UI。

首先,这个看起来怎么样?这个很重要!

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun Counter(start: Int, stop: Int): Int {
val value by remember { mutableStateOf(start) }

LaunchedEffect(Unit) {
while (value <= stop) {
delay(1_000)
value++
}
}

return value
}

这就是一个普通的 composable 函数,返回一个状态,可以用于绑定到 Compose UI 的 text 属性上。

Molecule 可以让你以 composable 的形式,将其转换成 StateFlow<Int> 在其他地方使用。当第一个值初始化后, Compose 同步 recompose,所有后续的值都会 emit 到 StateFlow。

在 presenter,我们可以在 Molecule 中使用可组合的函数模式:

1
2
3
4
5
6
7
8
@Composable
fun SomePresenter(events: Flow<EventType>): ModelType {
// ...
}

val models: StateFlow<ModelType> = scope.launchMolecule {
SomePresenter(events)
}

Compose 为我们开启了一种新的方式,去实现逻辑。编译器插件的使用,以一种原始协程库 API 无法实现的方式解锁了这门语言。关于 Compose 可以看官方文档

现在,我们可以不必再写一堆 RxJava 或是 Flow 操作符,我就写 if/elsefor 循环,不必再用 publish/filter/merge 组合,直接用 when 语句,还能有 exhaustiveness 语法检查。

Compose 提供的工具,比如 remember,state,derived state,effects 等等我们都可以继续用。Molecule 的示例项目中有稍微复杂的使用场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Composable
fun CounterPresenter(
events: Flow<CounterEvent>,
randomService: RandomService,
): CounterModel {
var count by remember { mutableStateOf(0) }
var loading by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is Change -> {
count += event.delta
}
Randomize -> {
loading = true
launch {
count = randomService.get(-20, 20)
loading = false
}
}
}
}
}

return CounterModel(count, loading)
}

在 Cash App 的实际使用中是基于类的,可以标准化 presenter API,仍然可以享受编译时安全的依赖注入。

1
2
3
4
5
6
7
8
class CounterPresenter @Inject constructor(
private val randomService: RandomService,
) : MoleculePresenter {
@Composable
override fun Present(events: Flow<CounterEvent>) : CounterModel {
// ...
}
}

我们使用 Molecule 已经有 5 个月了。它还没有为正式版做好准备,我们没有 100% 确定应该以怎样的形式使用 Compose API。我们在这周公布了这个库,集成到 Cash App 进行更多的测试。邀请你来加入我们,体验这个库。

这会是我们管理状态的最终形式吗?未必。但这是我们的下一个方案,也可能是你的~