”总所周知“,在 Compose 中有个思想叫做状态提升,在之前的文章Compose学习笔记2 - LaunchedEffect、状态与 状态管理中我们曾提及过。
状态提升的目的是为了让我们的组件尽可能的”无状态“,无状态的优点:
- 可复用,组件只负责组件的职责,不持有或者少持有状态
- 可测试,组件不持有状态,更接近于纯函数,相同输入必然有相同输出
状态提升的想法很好,但是实践的时候可能并不美妙。
可能有点丑陋的状态提升
我们快速的写一个 TodoList,来演示一下状态提升可能存在的问题:
@Composable
fun TestStateHoisting() {
// 在顶层组件声明状态与改变状态的函数(称之为事件)
val list = useList<Todo>()
fun addTodo(todo: Todo) {
list.add(todo)
}
fun delTodo(id: String) {
list.removeIf { it.id == id }
}
Surface {
Column {
//事件传递
Header(::addTodo)
TodoList(todos = list, ::delTodo)
}
}
}
data class Todo(val name: String, val id: String)
@Composable
fun Header(addTodo: (Todo) -> Unit) {
val (input, setInput) = useState("")
Row {
OutlinedTextField(
value = input,
onValueChange = setInput,
)
TButton(text = "add") {
addTodo(Todo(input, NanoId.generate()))
setInput("")
}
}
}
@Composable
fun TodoList(todos: List<Todo>, delTodo: (String) -> Unit) {
Column {
todos.map {
TodoItem(item = it, delTodo)
}
}
}
@Composable
fun TodoItem(item: Todo, delTodo: (String) -> Unit) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = item.name)
TButton(text = "del") {
delTodo(item.id)
}
}
}
@Composable
fun TButton(
text: String,
enabled: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Button(onClick = onClick, enabled = enabled, modifier = modifier.padding(PaddingValues(4.dp))) {
Text(text = text)
}
}
这是一个非常完整的 ”状态提升“ 示例,但是它有一点点丑陋。例子中这种组织、管理状态的思想称之为:单向数据流,即状态(数据)从父组件向下流向子组件,数据只有一个唯一可信源,就是来自父组件的状态。子组件从过向上传递事件(通过调用由父组件传递的改变状态的函数实现传递),来改变状态。
使用状态提升,在面对一些复杂场景,例如多个不同层级的组件,需要将所有状态提升到共有的顶层组件,然后通过 props 在组件之间传递。一来代码量上提升很多,二来如果涉及修改,就会比较麻烦。
有的中间组件可能并不需要使用这些状态,或者函数。例如 TodoList
组件,在它的实现中它其实并不关心 delTodo
函数到底是什么,它也不会使用这个函数。但是为了传递到目标组件还是需要在 props 中进行声明,显得非常的笨重。
使用 useContext
来解耦组件之间的状态、事件传递
上面的例子我们只传递了两层,Root -> TodoList -> TodoItem,实际开发可能会存在更多的状态传递层级,还用这种方式显然有些笨拙了。
我们还有其他方法么?当然,我们还可以使用 ViewModel,通过它持有状态、改变状态的函数,这都很好,很符合开发 Android 的既往路线。
但是我们还可以试一试更好玩的方法,使用junerver/ComposeHooks 中的 useContext
函数,在无需创建 vm 文件的情况下,更函数式的处理状态。
改造第一步:创建上下文
首先使用 createContext
创建一个上下文对象,同时传入默认值:
val TodoContext = createContext(tuple(
emptyList<Todo>(), // 对应list状态
{ _: Todo -> }, // 对应 addTodo函数
{ _: String -> } // 对应 delTodo函数
))
这里我们传入的都是空值、空函数,tuple函数是我自定义的快速创建 Triple
的函数。
改造第二步:使用上下文对象提供的 Provider
组件
@Composable
fun TestStateHoisting() {
val list = useList<Todo>()
fun addTodo(todo: Todo) {
list.add(todo)
}
fun delTodo(id: String) {
list.removeIf { it.id == id }
}
// 在这个组件之下的所有组件都能使用我们暴露出的这三个内容
TodoContext.Provider(
value = tuple(
list,
::addTodo,
::delTodo
)
) {
Surface {
Column {
// Header、TodoList 都改造成无参组件
Header()
TodoList()
}
}
}
}
改造第三步:改造子组件,使用 useContext
函数获取需要的状态、函数
@Composable
fun Header() {
// 传入上下文,使用解构声明拿到对应顺序的函数
val (_, addTodo) = useContext(context = TodoContext)
val (input, setInput) = useState("")
Row {
OutlinedTextField(
value = input,
onValueChange = setInput,
)
TButton(text = "add") {
addTodo(Todo(input, NanoId.generate()))
setInput("")
}
}
}
@Composable
fun TodoList() {
// 拿到的todos本身就是状态,可以直接使用
val (todos) = useContext(context = TodoContext)
Column {
todos.map {
TodoItem(item = it)
}
}
}
@Composable
fun TodoItem(item: Todo) {
// 不使用的解构声明对象,可以使用`_` 作为占位符
val (_, _, delTodo) = useContext(context = TodoContext)
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = item.name)
TButton(text = "del") {
delTodo(item.id)
}
}
}
完成:现在我们的组件互相之间不再耦合,无需传递状态、函数
对比改造前后,我们再也不用关心状态的传递,后续代码更新也不用担心牵一发而动全身。
总结:
- 使用
createContex
创建上下文对象 - 使用
上下文对象.Provider
作为根组件 - 在需要使用状态、函数的组件中使用
useContext(上下文对象)
获取
探索更多
项目开源地址:junerver/ComposeHooks
MavenCentral:hooks
implementation("xyz.junerver.compose:hooks:1.0.8")
欢迎使用、勘误、pr、star。