在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。

相关推荐
薛晓刚4 分钟前
MySQL的replace使用分析
android·adb
DengDongQi27 分钟前
Jetpack Compose 滚轮选择器
android
stevenzqzq28 分钟前
Android Studio Logcat 基础认知
android·ide·android studio·日志
代码不停36 分钟前
MySQL事务
android·数据库·mysql
朝花不迟暮42 分钟前
使用Android Studio生成apk,卡在Running Gradle task ‘assembleDebug...解决方法
android·ide·android studio
yngsqq1 小时前
使用VS(.NET MAUI)开发第一个安卓APP
android·.net
前端小臻1 小时前
列举react中类组件和函数组件常用到的方法
前端·javascript·react.js
wayne2141 小时前
React Native 状态管理方案全梳理:Redux、Zustand、React Query 如何选
javascript·react native·react.js
Android-Flutter1 小时前
android compose LazyVerticalGrid上下滚动的网格布局 使用
android·kotlin
Android-Flutter1 小时前
android compose LazyHorizontalGrid水平滚动的网格布局 使用
android·kotlin