文章目录
- [状态与 Compose 的核心概念](#状态与 Compose 的核心概念)
- [声明和存储状态的核心 API](#声明和存储状态的核心 API)
- 有状态、无状态与状态提升
-
- [有状态 vs 无状态](#有状态 vs 无状态)
- 状态提升
- 状态容器
状态与 Compose 的核心概念
-
什么是状态?
在应用中,状态是指任何可以随时间变化的值。范围非常广,大到 Room 数据库的数据,小到类的变量(如:网络连接提示、展开/收起的面板、输入框中的文字)。
-
状态与组合 (Composition) 的关系
声明式 UI:Compose 是声明式的,更新界面的唯一方法是使用新的参数调用同一可组合项(Composable 函数)。
重组 (Recomposition):当界面的状态发生变化时,Compose 会触发重组,使用新状态重新执行对应的可组合函数来更新 UI。
必须明确感知:普通的 Kotlin 变量被修改时不会触发 Compose 更新。可组合项必须被显式告知状态的改变(例如使用 MutableState),才能触发重组。
声明和存储状态的核心 API
- 创建一个可被 Compose 观察的 MutableState。当它的值发生改变时,Compose 会自动安排读取了该状态的可组合项进行重组。
- 单纯使用 mutableStateOf 每次重组都会重新初始化状态。为了让状态在重组中"存活"下来,必须使用 remember。
在可组合项中声明 MutableState 对象的方法有三种:
kotlin
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
// 解构成两个变量,value是值, setValue 是 函数变量, 即 setValue(value)
val (value, setValue) = remember { mutableStateOf(default) }
配置更改后保持、恢复状态
remember 只能在重组时保持状态。如果要在重新创建 activity 或进程之后保留状态。需使用 rememberSaveable 。
- 自动保存类型:只要是能存入 Android Bundle 的数据类型,它都能自动保存。
kotlin
@Composable
fun ChatBubble(
message: Message
) {
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
- 如果需要用 rememberSaveable 保存无法直接放入 Bundle 的自定义对象。那可以放入实现了 Parcelable 序列化的对象
kotlin
// 1. 添加 @Parcelize 注解并实现 Parcelable
@Parcelize
data class UserState(val name: String, val age: Int) : Parcelable
@Composable
fun ParcelableExample() {
// 2. 直接使用 rememberSaveable
var user by rememberSaveable { mutableStateOf(UserState("张三", 18)) }
Button(onClick = { user = user.copy(age = user.age + 1) }) {
Text("姓名: ${user.name}, 年龄: ${user.age} (点击长大)")
}
}
- 保存无法修改源码的第三方类(使用自定义 Saver)
如果对象无法实现 Parcelable(例如它是第三方库中的类),可使用 mapSaver 或 listSaver 自定义保存和恢复的逻辑。
mapSaver 保存多个属性的示例:
kotlin
import androidx.compose.runtime.saveable.mapSaver
// 假设这是一个第三方类,有很多属性
data class ComplexUser(
val id: Long,
val name: String,
val age: Int,
val isVip: Boolean,
val avatarUrl: String
)
val ComplexUserMapSaver = run {
// 提前定义好所有的 key
val idKey = "id"
val nameKey = "name"
val ageKey = "age"
val vipKey = "vip"
val avatarKey = "avatar"
mapSaver(
save = { user ->
// 把所有需要保存的属性都塞进 map 里
mapOf(
idKey to user.id,
nameKey to user.name,
ageKey to user.age,
vipKey to user.isVip,
avatarKey to user.avatarUrl
)
},
restore = { map ->
// 从 map 里把所有的属性取出来,重新构建对象
ComplexUser(
id = map[idKey] as Long,
name = map[nameKey] as String,
age = map[ageKey] as Int,
isVip = map[vipKey] as Boolean,
avatarUrl = map[avatarKey] as String
)
}
)
}
listSaver 保存多个属性的示例 :
不需要定义 Key,只需要保证存进去的顺序和取出来的顺序完全一致即可
kotlin
import androidx.compose.runtime.saveable.listSaver
val ComplexUserListSaver = listSaver<ComplexUser, Any>(
save = { user ->
// 按照顺序把属性放进 List 里
listOf(user.id, user.name, user.age, user.isVip, user.avatarUrl)
},
restore = { list ->
// 严格按照上面 save 的索引顺序取出来
ComplexUser(
id = list[0] as Long,
name = list[1] as String,
age = list[2] as Int,
isVip = list[3] as Boolean,
avatarUrl = list[4] as String
)
}
)
其他受支持的可被compose观察的状态类型
除 MutableState<T> 外,对以下这些数据类型,提供了扩展函数,以转换成可被观察的 State<T>
- Flow 数据, collectAsStateWithLifecycle() 以生命周期感知型方式从 Flow 收集值
- Flow:collectAsState(),不感知生命周期
- LiveData: observeAsState()
- RxJava2: subscribeAsState()
- RxJava3: subscribeAsState()
这些扩展函数,需要特定的依赖lib
implementation("androidx.lifecycle:lifecycle-runtime-compose")
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.compose.runtime:runtime-rxjava2")
implementation("androidx.compose.runtime:runtime-rxjava3")
还可以对其它任何可观察的类、自定义的可观察类,使用 produceState API 对其进行转换,以生成 State<T>。
有状态、无状态与状态提升
有状态 vs 无状态
-
有状态:在内部使用 remember 创建并管理自己状态的可组合项。调用方不需要关心它的状态,但缺点是不易复用、更难测试。
-
无状态:不包含任何内部状态,状态完全由外部调用方通过参数传入。
状态提升
这是 Compose 中的核心设计模式,将有状态组件变为无状态组件。
状态未提升的示例:
kotlin
@Composable
fun HelloContent() {
var name by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") })
}
}
状态提升 的常规实现是将内部状态变量替换为两个参数:
value: T:当前应该显示的值。
onValueChange: (T) -> Unit:请求改变值的回调事件(T 是新值)。
针对上面的示例,增加一个父可组合项 HelloScreen,将 name 状态 ,提升到 HelloScreen 中,状态改变的回调事件,也提升到父项中传入:
状态提升示例:
kotlin
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
}
}
优势:状态提升后,HelloContent 将变得可复用
并不总是需要提升状态。当其他可组合项不需要控制状态时,可以将状态保留在可组合项内部。
状态容器
状态容器 负责管理可组合函数的状态和逻辑 。它的核心作用是关注点分离,将复杂的状态管理和行为逻辑从 UI 层抽离出来,让可组合函数保持纯粹、简洁,仅专注于如何渲染 UI。
随着 UI 的复杂度增加,如果将所有的状态(如展开/收起状态、滚动位置)和逻辑(如网络请求、数据校验)都写在 Composable 内部,代码会变得极其臃肿且难以测试。状态容器就是为了解决这个问题而存在的。
在 Compose 中,状态容器通常分为两种主要类型,分别负责不同层级的逻辑
普通的状态容器类
用于管理 UI 逻辑 和 UI 元素状态。
它通常是一个普通的 Kotlin 类,生命周期与它所在的 Composition 绑定。
- 适用场景:处理动画状态、抽屉导航(Drawer)的打开/关闭、列表的滚动位置、控制 Snackbar 的显示等。
- 实现方式:通常会配合一个带有 remember 的工厂函数来创建和缓存该容器实例。
示例:
kotlin
// 1. 定义状态容器类
class MyAppState(
val scaffoldState: ScaffoldState,
val coroutineScope: CoroutineScope
) {
// 封装 UI 逻辑:打开侧边栏
fun openDrawer() {
coroutineScope.launch {
scaffoldState.drawerState.open()
}
}
}
// 2. 提供一个 remember 函数供 Composable 使用
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
coroutineScope: CoroutineScope = rememberCoroutineScope()
): MyAppState {
return remember(scaffoldState, coroutineScope) {
MyAppState(scaffoldState, coroutineScope)
}
}
// 3. 在 UI 中使用状态容器
@Composable
fun MyApp() {
// 获取状态容器实例
val appState = rememberMyAppState()
Scaffold(
scaffoldState = appState.scaffoldState,
drawerContent = { /* 侧边栏内容 */ }
) {
Button(onClick = { appState.openDrawer() }) {
Text("打开侧边栏")
}
}
}
ViewModel
用于管理 业务逻辑 和 屏幕级别的状态
其生命周期比普通的 Composable 更长,能够在使用配置更改(如屏幕旋转)时存活下来。
- 适用场景:发起网络请求、读取数据库、处理用户登录等核心业务逻辑,以及保存屏幕的主数据状态。
- 实现方式:继承 androidx.lifecycle.ViewModel,内部使用 StateFlow 或 Compose 的 State 来暴露数据。
示例:
kotlin
class UserViewModel : ViewModel() {
// 暴露给 Compose 的屏幕状态
var userName by mutableStateOf("")
private set
// 封装业务逻辑
fun fetchUser() {
viewModelScope.launch {
// 模拟网络请求
userName = "张三"
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
// Composable 只负责渲染 ViewModel 提供的数据,以及将事件传递给 ViewModel
Column {
Text("当前用户: ${viewModel.userName}")
Button(onClick = { viewModel.fetchUser() }) {
Text("获取用户数据")
}
}
}