Android Compose 状态:核心api,状态恢复,状态提升,状态容器

文章目录

状态与 Compose 的核心概念

  1. 什么是状态?

    在应用中,状态是指任何可以随时间变化的值。范围非常广,大到 Room 数据库的数据,小到类的变量(如:网络连接提示、展开/收起的面板、输入框中的文字)。

  2. 状态与组合 (Composition) 的关系

    声明式 UI:Compose 是声明式的,更新界面的唯一方法是使用新的参数调用同一可组合项(Composable 函数)。

    重组 (Recomposition):当界面的状态发生变化时,Compose 会触发重组,使用新状态重新执行对应的可组合函数来更新 UI。

    必须明确感知:普通的 Kotlin 变量被修改时不会触发 Compose 更新。可组合项必须被显式告知状态的改变(例如使用 MutableState),才能触发重组。


声明和存储状态的核心 API

  1. 创建一个可被 Compose 观察的 MutableState。当它的值发生改变时,Compose 会自动安排读取了该状态的可组合项进行重组。
  2. 单纯使用 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("获取用户数据")
        }
    }
}
相关推荐
千里马学框架3 小时前
干货分享:车载音频audio调试开发之dumpsys CarAudioService剖析
android·音视频·面试题·audio·系统开发·车载audio·framework工程师
私人珍藏库3 小时前
[Android] Bilibili音乐播放器Biu安卓版,适配手机-平板-汽车
android·智能手机·电脑
z9209810233 小时前
安卓16 手机系统 super 分区挂载读写 解锁安卓动态分区RW 逻辑分区读写 自由读写 修改分区system vendor、product
android·智能手机·安卓动态分区·解锁动态分区·分区挂载读写
风酥糖3 小时前
Android上部署Linux环境的方案总结对比
android·linux·运维
行思理3 小时前
mac终端下生成android 证书
android·macos
今夕资源网3 小时前
LANDrop 跨平台局域网文件传输工具 支持 iOS、Android、macOS、Windows、Linux、Android TV 等操作系统
android·linux·macos·跨平台·文件传输·局域网文件共享·局域网文件传输
BduL OWED14 小时前
mysql的主从配置
android·mysql·adb
人需要PID14 小时前
【卡尔曼工具箱-EKF-MATLAB应用】
android
亘元有量-流量变现16 小时前
APP自动识别跳转各大应用商店(鸿蒙+iOS+安卓全品牌)|可直接部署落地页源码
android·ios·harmonyos