在Compose中使用状态提升?我提升个P...Provider

原文地址:# 在Compose中使用状态提升?我提升个P...Provider

"总所周知",在 Compose 中有个思想叫做状态提升 ,在之前的文章Compose学习笔记2 - LaunchedEffect、状态与 状态管理中我们曾提及过。

状态提升的目的是为了让我们的组件尽可能的"无状态",无状态的优点:

  • 可复用,组件只负责组件的职责,不持有或者少持有状态
  • 可测试,组件不持有状态,更接近于纯函数,相同输入必然有相同输出

状态提升的想法很好,但是实践的时候可能并不美妙。

可能有点丑陋的状态提升

我们快速的写一个 TodoList,来演示一下状态提升可能存在的问题:

kotlin 复制代码
@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 创建一个上下文对象,同时传入默认值:

kotlin 复制代码
val TodoContext = createContext(tuple(
    emptyList<Todo>(), // 对应list状态
    { _: Todo -> }, // 对应 addTodo函数 
    { _: String -> } // 对应 delTodo函数
))

这里我们传入的都是空值、空函数,tuple函数是我自定义的快速创建 Triple 的函数。

改造第二步:使用上下文对象提供的 Provider 组件

kotlin 复制代码
@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 函数获取需要的状态、函数

kotlin 复制代码
@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)
        }
    }
}

完成:现在我们的组件互相之间不再耦合,无需传递状态、函数

对比改造前后,我们再也不用关心状态的传递,后续代码更新也不用担心牵一发而动全身。

总结:

  1. 使用 createContex 创建上下文对象
  2. 使用 上下文对象.Provider 作为根组件
  3. 在需要使用状态、函数的组件中使用 useContext(上下文对象)获取

探索更多

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

kotlin 复制代码
implementation("xyz.junerver.compose:hooks:1.0.3")

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

相关推荐
小林爱2 小时前
【Compose multiplatform教程06】用IDEA编译Compose Multiplatform常见问题
android·java·前端·kotlin·intellij-idea·compose·多平台
掘金酱8 小时前
稀土掘金社区2024年度影响力榜单正式公布
android·前端·后端
二流小码农8 小时前
鸿蒙开发:自定义一个英文键盘
android·ios·harmonyos
二流小码农8 小时前
鸿蒙开发:自定义一个股票代码选择键盘
android·ios·harmonyos
kim56599 小时前
android studio 写一个小计时器
android·ide·android studio
m0_674031439 小时前
React - useContext和深层传递参数
前端·javascript·react.js
傻小胖9 小时前
React Diffing 算法完整指南
开发语言·react.js
刺客-Andy9 小时前
React 第二十节 useRef 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
漂流瓶jz9 小时前
如何使用React,透传各类组件能力/属性?
前端·javascript·react.js
JasonYin~10 小时前
HarmonyOS NEXT 实战之元服务:静态多案例效果(一)
android·华为·harmonyos