Compose 学习界面架构 - State 状态管理

上一章附带效应(Side Effects)中反复提到了一个组件:State,它是 Compose 当中至关重要的一个组件,本章将了解 State 的作用与用法。

官方文档:官方文档

在开始之前,可以运行一下这段输入框(与XML中EditText类似)的代码:

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Hello!", style = MaterialTheme.typography.bodyMedium)
        
        OutlinedTextField(
            value = "", 
            onValueChange = {}, 
            label = { Text("Name") }
        )
    }
}

运行后会发现:无论怎么输入,文本框都没反应。原因在了解 Compose 声明式的思维后就明白了。

一、Compose 和传统的 View 的思维差异

在传统的 View 中更多是一种命令式的思维,在设置一个 View 的时候更多只会设置它的初始状态,而在更新时需要主动 “命令” 控件更新,比如:setText。

而在 Compose 中更多是一种声明式的思维,只需要 “描述” 你想要的界面(比如 Text(text = 变量名)),界面的更新完全依赖 “状态变量” 的变化。如果状态没变化,界面就不会变;如果状态变了,Compose 会自动 “重组”(重新执行可组合函数)来更新界面。

所以状态就是:可以随时间变化的任何值。比如:

  • 文本输入框里用户输入的内容(TextField 的 value)
  • 网络断开时显示的 “无网络” 提示(布尔值 isNoNetwork)
  • 列表的滚动位置(LazyListState)
  • 聊天气泡是否展开(布尔值 showDetails)

所以在上面的例子中,输入框的内容不会变化的原因就是:value 是固定的空字符串,没有任何 “状态变量” 记录用户输入 —— 状态没变化,Compose 不会重组,界面自然不更新。

那么该如何声明状态变量?

二、三种声明状态的方式(官方推荐)

这三种写法本质上都是创建 MutableState<T>,三种写法都是等效的。

方式1、直接声明 MutableState 对象

适合需要明确操作 “状态容器” 的场景(比如传递给其他函数):

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        // 用 remember 包裹 mutableStateOf,初始值为空字符串
        val name = remember { mutableStateOf("") }
        
        if (name.value.isNotEmpty()) { // 读取状态
            Text(text = "Hello, ${name.value}!")
        }
        
        OutlinedTextField(
            value = name.value, // 读取状态
            onValueChange = { newName -> 
                name.value = newName // 修改状态:给 .value 赋值
            },
            label = { Text("Name") }
        )
    }
}

mutableStateOf()函数就是用于创建一个可变的State对象,参数中传入的是初始值。如果想要访问这个State对象中的值,那么就调用它的getValue()函数,想要更新State对象中的值,就调用它的setValue()函数。

而 remember 函数的作用是让其包裹住的变量在重组的过程中得到保留,从而就不会出现变量被重新初始化的情况了

可以将 remeber 删除后运行看看效果:

 val nameState = mutableStateOf("")

可以发现如果去掉 remember 函数,无论怎么输入,文本框也都没反应。这里的 State 其实已经是正常工作了,因为 Compose 是通过重组让界面更新的,当 HelloContent() 函数每次重组的时候,nameState 的值都被重新初始化成了空,所以在使用 mutableStateOf 都配套结合 remember 使用即可

 方式 2、简化 State 语法使用 by 委托

使用 by 关键字替代之前的等号,用委托的方式来为 name 变量赋值。

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        // by 委托:直接声明变量,读写不用 .value
        var name by remember { mutableStateOf("") }
        
        if (name.isNotEmpty()) { // 直接读
            Text(text = "Hello, $name!")
        }
        
        OutlinedTextField(
            value = name, // 直接读
            onValueChange = { name = it }, // 直接改
            label = { Text("Name") }
        )
    }
}

在方式一中,使用等号赋值的时候 name 的类型是 MutableState<String>,而改用 by 关键字赋值之后,name 的类型就变成了 String,那么就可以直接对这个值进行读写操作了,而不用像之前那样调用它的 getValue() 和 setValue() 函数。

不过使用 by 关键字需要导入:

// 必须导入这两个包
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

方式 3:解构(value + setValue)

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        // 解构:第一个参数是状态值,第二个是修改函数
        val (name, setName) = remember { mutableStateOf("") }
        
        if (name.isNotEmpty()) {
            Text(text = "Hello, $name!")
        }
        
        OutlinedTextField(
            value = name,
            onValueChange = { setName(it) }, // 调用修改函数
            label = { Text("Name") }
        )
    }
}

在使用 State 时需要注意:

1、remeber 只在组合内有效,如果包含 remeber 的可组合项被移除(比如从屏幕上消失),remeber 保存的状态会被 “忘记”。

2、不要用普通可变对象当状态:比如 ArrayList、mutableListOf()—— 这些对象变化时,Compose 不可观察,不会触发重组。正确做法是用 State<List<T>> + 不可变列表 listOf()。

三、状态保存

使用 remeber 配合 mutableListOf 已经实现了输入对应的内容后显示,已经可以正常工作了,但是其实还存在一个问题,每当手机横竖屏旋转的时候,已经输入的内容都会清空。这是因为旋转屏幕(配置更改),remember 保存的状态会丢失 —— 因为旋转屏幕时 Activity 会重建,Compose 的 “组合” 也会重新创建,remember 会重新初始化。

这个问题解决很简单,只需要将 remember 替换为增强版:rememberSaveable ,它能在 “配置更改”(旋转屏幕)和 “系统杀死进程” 后保留状态。因为 rememberSaveable 会把状态存储在 Bundle 中。

rememberSaveable 的用法

场景 1 、基础类型

对于 String、Int、Boolean 等基础类型,rememberSaveable 能自动保存,直接替换 remember 即可:

@Composable
fun HelloScreen() {
    // 用 rememberSaveable 替换 remember,旋转屏幕不丢失
    var name by rememberSaveable { mutableStateOf("") }
    
    HelloContent(name = name, onNameChange = { name = it })
}

场景 2 、自定义类型

如果状态是自定义数据类(比如 City(name: String, country: String)),rememberSaveable 无法自动保存,需要用以下三种方式:

1、给数据类加 @Parcelize 注解

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    // 用 rememberSaveable 保存
    var selectedCity by rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

2、如果不能用 @Parcelize(比如数据类来自第三方库),可以用 mapSaver 定义 “如何把对象转成键值对”:

data class City(val name: String, val country: String)

// 定义 MapSaver:指定保存和恢复逻辑
val CitySaver = run {
    val nameKey = "city_name" // 键1
    val countryKey = "city_country" // 键2
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) }, // 保存:对象→Map
        restore = { City(it[nameKey] as String, it[countryKey] as String) } // 恢复:Map→对象
    )
}

@Composable
fun CityScreen() {
    // 传递 saver 参数
    var selectedCity by rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

3、ListSaver用索引代替键,和 MapSaver 类似,用列表的索引代替键,适合字段少的场景:

data class City(val name: String, val country: String)

// 定义 ListSaver:按索引保存
val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) }, // 保存:对象→List
    restore = { City(it[0] as String, it[1] as String) } // 恢复:List→对象(按索引取)
)

@Composable
fun CityScreen() {
    // 传递 saver 参数
    var selectedCity by rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

注意:

  • rememberSaveable 存储的状态不能太大:因为 Bundle 有大小限制(约 1MB),存太大可能导致 TransactionTooLarge 异常。
  • 只存 “关键信息”:比如列表的 “当前选中 ID”,而不是整个列表。

四、状态提升 - 规范编程

目前位置 State 的用法已经全部了解,但官方有一个强烈建议的 Compose 核心模式 - 状态提升。

在了解状态提升前需要明确两个概念,有状态可组合项 与 无状态可组合项:

类型定义优缺点
有状态可组合项自己用 remember 存储状态

优点:调用简单;

缺点:不可复用、难测试、状态逻辑耦合

无状态可组合项不存储状态,状态由 “调用方传递进来”

优点:可复用、易测试、状态逻辑解耦;

缺点:调用方需要管理状态

通过这两个概念,也就清楚了状态提示的目标就是:将可组合项改造为无状态,把状态 “提” 到调用方

清楚这个目标后,还有一个问题,函数的调用有时候是层层嵌套的,那么应该将 State 提升到哪一个父函数才算结束呢?官方给出了三条规则:

  1. 提至最低共同父项:如果有多个 Compose 函数都需要读取同一个 State 对象,那么 State 要提给所有需要 “读” 这个状态的 Compose 函数的共同父项(避免状态冗余)。
  2. 提至可修改的最高级别:如果有多个 Composable 函数需要对同一个State对象进行写入,那么 State 要提给所有能写入这个 State 函数的最高级别(确保单一修改入口)。
  3. 同事件触发的状态一起提:如果某个事件的触发会导致两个或更多的 State 发生变更(比如点击按钮同时改 “文本” 和 “是否显示提示”),那么这些 Sate 要一起提到相同的层级。

记住这三条规则后,就可以着手修改 HelloContent 函数了:

// 把 HelloContent 的 name 状态,提升到它的调用方 HelloScreen 中:
// 有状态可组合项:管理状态,传递给无状态的 HelloContent
@Composable
fun HelloScreen() {
    // 在这里管理状态
    var name by remember { mutableStateOf("") }

    // 传递状态值和修改函数
    HelloContent(
        name = name,
        onNameChange = { newName ->
            // 这里可以加额外逻辑(比如限制输入长度)
            if (newName.length <= 10) {
                name = newName
            }
        }
    )
}


// 无状态可组合项:状态由调用方传递
@Composable
fun HelloContent(
    name: String, // 接收状态值(只读)
    onNameChange: (String) -> Unit // 接收状态修改函数(触发修改)
) {
    Column(modifier = Modifier.padding(16.dp)) {
        if (name.isNotEmpty()) {
            Text(text = "Hello, $name!", style = MaterialTheme.typography.bodyMedium)
        }
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange, // 调用传入的修改函数
            label = { Text("Name") }
        )
    }
}

通过 HelloContent 的改造,想必就已经清楚状态提升的好处了:

1、HelloContent 可以在其他地方复用

2、调用方可以在修改状态前加逻辑(比如上面的 “限制 10 个字符”)

3、只有 HelloScreen 能修改 name,避免多个地方改状态导致的 bug。

五、小结

  • State 的本质:随时间变化的值、界面更新的唯一触发源。
  • 基础 API 组合:remember(重组保留)+ mutableStateOf(重组更新),需要时用 rememberSaveable(配置更改保留)。
  • 状态提升规则:最低共同父项、可修改最高级别、同事件状态一起提。
  • 状态保存场景:基础类型直接用 rememberSaveable,自定义类型用 @Parcelize/MapSaver/ListSaver。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值