”总所周知“,在 Compose 中有个思想叫做状态提升,在之前的文章Compose学习笔记2 - LaunchedEffect、状态与 状态管理中我们曾提及过。
状态提升的目的是为了让我们的组件尽可能的”无状态“,无状态的优点:
- 可复用,组件只负责组件的职责,不持有或者少持有状态
- 可测试,组件不持有状态,更接近于纯函数,相同输入必然有相同输出
状态提升的想法很好,但是实践的时候可能并不美妙。
可能有点丑陋的状态提升
我们快速的写一个 TodoList,来演示一下状态提升可能存在的问题:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @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
创建一个上下文对象,同时传入默认值:
1 2 3 4 5
| val TodoContext = createContext(tuple( emptyList<Todo>(), { _: Todo -> }, { _: String -> } ))
|
这里我们传入的都是空值、空函数,tuple函数是我自定义的快速创建 Triple
的函数。
改造第二步:使用上下文对象提供的 Provider
组件
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 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() } } } }
|
改造第三步:改造子组件,使用 useContext
函数获取需要的状态、函数
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 28 29 30 31 32 33 34 35 36 37 38 39
| @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() { 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
1
| implementation("xyz.junerver.compose:hooks:1.0.3")
|
欢迎使用、勘误、pr、star。