在Compose中方便的使用MVI思想?试试useReducer!

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关系复杂的状态管理,专心于业务与UI组件。

这是系列文章的第四篇,前文:

什么是 MVI?

什么是 MVI?想必你也看过很多博客了,其实简单说就是:明确分离数据模型(Model)、用户界面(View)和用户意图(Intent,也称为事件、动作),以实现UI的响应式和可预测的更新

它与 MVVM 其实区别不大,有别于 MVVM 的是,MVVM 将耦合代码按照职责区分,拆分文件。借助 LiveData 或者 DataBinding 将 VM 中数据更新直接驱动 V 层,实现了 V 层与 M 层之间的解耦。

得力于 Compose 带来的状态驱动视图能力,我们可以理解 MVI 思想为:用户发出事件,事件驱动状态变化,状态驱动 UI 变化。这也就是所谓的事件向上,状态向下:事件从组件发出,单一可信来源的状态驱动组件更新。

从这一思想出发,我们可以理解为:谁持有状态,谁就是 M 层。那么过去的 MVVM 的文件拆分将会变得相对松散,我们完全可以摒弃过去那种全屏式思想,不再根据屏幕创建一个 VM 大管家。而是拆分成职责、粒度更细小的组件思想。

事件向上状态向下

当然使用 MVVM 我们一样可以做到类似的效果,但是 MVI 将它流程化、标准化,所以可以理解为其实 MVI 就是一个有一定模板的更优秀的 MVVM

过去我们的 VM 层其实也很重,一个复杂的页面,数个网络接口,都被仍在一个 VM 中,鉴于不同开发者的水平的参差,我们项目中甚至有一个 VM 文件中持有了20多个 LiveData,可以说完全违背了 MVVM 的初衷。

同样的,想必你已经看了很多在 Compose 中使用 ViewModel 来实现 MVI 的文章了吧(我甚至看过回字的四种写法),它真的有这么复杂么?在 Compose 中我们还需要这样的一位大管家么?虽然很多例子、甚至官方的 demo,都还在使用 ViewModel,但是这是一种无法回避的取舍?还是既往路线的惯性。

在一些场景下,我们完全可以更组件化思维,在更小粒度上应用 MVI。今天你可以试试一点新东西:useReducer,通过它我将进一步阐述我所说的:松散的、组件下的 MVI 思想。

我们需要 VM 么?

MVI 相关文章中你可能会看到一个观点:纯函数(给定相同的输入时,总是产生相同的输出,并且不产生任何副作用的函数)。

我们构建一个改变状态的函数,称之为 reducer 函数,将上一个状态、Intent(也成为 event、action)作为函数的入参,将返回值作为新的状态应用于组件,只要这个 reducer 函数是 纯函数,我们就实现了:可预测的更新

现在我们来构建一个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 构建状态类型
data class SimpleData(
val name: String,
val age: Int,
)

// Intent、我们一半习惯称之为:action,使用 sealed interface可以方便的实现
sealed interface SimpleAction {
data class ChangeName(val newName: String) : SimpleAction
data object AgeIncrease : SimpleAction
}

// 构建一个 Reducer 函数,泛型是状态的类型
val simpleReducer: Reducer<SimpleData> = { prevState: SimpleData, action: Any ->
when (action) {
is SimpleAction.ChangeName -> prevState.copy(name = action.newName)
is SimpleAction.AgeIncrease -> prevState.copy(age = prevState.age + 1)
else -> prevState
}
}

reducer 函数中我们要使用 不可变 数据,data class 就是最好的选择,通过 copy 函数返回新的状态。

这些代码,要么是类型声明、要么是一个纯函数,他们与最终组件息息相关,但是他们并需要放到一个 ViewModel 类中,再想一想我们需要 ViewModel 么?

上面的代码几乎已经是 MVI 的完整实现了,M层状态:SimpleData,I层由Action、Reducer函数构成。

他们非常简单、容易理解,而且可以方便的扩展,规范了M层变化(你不能直接修改状态,必须通过传递 Action 给 Reducer 函数驱动状态变化)。

再来看看我们的 V 层需要做什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun UseReducerExample() {
val (state, dispatch) = useReducer(simpleReducer, initialState = SimpleData("default", 18))
val (input, setInput) = useState("")
Surface {
Column {
Text(text = "UserName: $state.name")
Text(text = "UserAge: $state.age")
OutlinedTextField(value = input, onValueChange = setInput)
TButton(text = "changeName") {
dispatch(SimpleAction.ChangeName(input))
}
TButton(text = "+1") {
dispatch(SimpleAction.AgeIncrease)
}
}
}
}

我们只需要使用:useReducer函数,传入 Reducer 函数与一个初始状态:initialState,通过解构声明语法,可以轻松的拿到状态dispatch函数。

然后在组件中使用即可。

我们不一定需要 VM!

现在我可以回答我之前的问题了,我们真的需要么?

很多场景我们其实并不需要,将状态从 VM 中拆解、粒化成为更小的一个个组件,在组件文件中直接声明这些状态类型、Action、Reducer 函数,然后通过 useReducer 函数即可。

在大多数场景也许我们根本就用不上 VM 带给我们的好处,它在 View 体系是那么重要,但是在 Compose 中,我认为有点可有可无了。

比如生命周期感知,除非你要使用旋转屏幕下的状态保持(大多数应用都是锁方向的)?

比如数据共享,了解一下状态提升?了解一下 useContext

如果你是旧的 MVVM 项目改造,那么使用 vm 改造成本比较小,如果你是新项目,我觉得一般的场景完全没有必要继续使用 VM 了。

探索更多

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
implementation("xyz.junerver.compose:hooks:1.0.6")

欢迎使用、勘误、pr、star。