写在前面
本文中提及的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 函数是 纯函数,我们就实现了:可预测的更新。
现在我们来构建一个最简单的例子:
// 构建状态类型
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 层需要做什么?
@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
implementation("xyz.junerver.compose:hooks:1.0.8")
欢迎使用、勘误、pr、star。