Jetpack Compose 状态管理

Jetpack Compose 状态管理

Jetpack Compose 正在改变我们构建 Android UI 的方式。其声明式的特性让我们能更直观地描述界面,但同时也带来了一个核心问题:如何管理和持久化UI状态?

Part 1: remember - Compose 的"短期记忆"

在 Compose 中,当一个状态改变时,相关的 Composable 函数会重组(Recomposition),也就是重新执行。这意味着,任何在函数体内的普通变量都会在每次重组时被重置。

为了解决这个问题,Compose 提供了 remember

remember 的核心作用是在多次重组之间"记住"一个值。它会将计算结果存储在组合树中,下次重组时直接返回缓存的值,而不是重新计算。

用例 1:记住 UI 状态

这是最常见的用法。rememberMutableState 结合,让状态在重组后得以保留。

kotlin 复制代码
// 如果没有 remember,每次重组 name 都会被重置为空字符串
var name by remember { mutableStateOf("") }```

#### 用例 2:缓存高成本计算

`remember` 不仅能记住宿主,还能记住任何计算结果,这对于避免重复执行昂贵操作至关重要。

​```kotlin
// 创建 ShaderBrush 的操作(特别是加载位图)是昂贵的
// 使用 remember 可以确保它只在首次组合时被创建一次
val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember 的局限 :它的记忆是"短期"的。当发生配置变更(如屏幕旋转)系统因内存不足终止进程 时,remember 记住的所有内容都会丢失。

Part 2: rememberSaveable - 跨越配置变更的"长期记忆"

为了解决 remember 的局限性,Compose 提供了 rememberSaveable

顾名思义,rememberSaveable 不仅能在重组中记住状态,还能自动将数据保存到 Android 的 Bundle 中。这意味着即使用户旋转了屏幕,甚至应用进程被系统回收后,状态也能够被完美恢复。

kotlin 复制代码
// 即使用户旋转屏幕,输入框中的文字也不会丢失
var name by rememberSaveable { mutableStateOf("") }

Part 3: 让自定义对象变得"可保存"

rememberSaveable 对基本数据类型(Int, String, Boolean...)支持得很好,但如果我们想保存一个自定义的 City 对象呢?默认情况下,rememberSaveable 是做不到的,因为它不知道如何将一个 City 对象存入 Bundle

我们有以下三种主流方案来解决这个问题。

方案一:@Parcelize - 自动化实现 (最推荐)

如果我们可以修改这个类的源码,这是最简单、最优雅的方式。通过添加 kotlin-parcelize 插件并为数据类添加 @Parcelize 注解,编译器会自动为我们实现 Parcelable 接口。

kotlin 复制代码
// 1. 在 build.gradle 中启用插件
plugins {
    id 'kotlin-parcelize'
}

// 2. 为数据类添加注解并实现 Parcelable 接口
@Parcelize
data class City(val name: String, val country: String) : Parcelable

// 3. 直接在 rememberSaveable 中使用
@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

rememberSaveable 会自动检测到 CityParcelable 的,并知道如何处理它。

方案二:自定义 Saver - 当你无法修改源码时

当我们无法修改类的定义时(例如,它来自第三方库),我们可以提供一个自定义的 Saver 对象,明确告诉 rememberSaveable 如何"保存"和"恢复"这个对象。

MapSaver - 健壮且可读的选择

MapSaver 将对象转换为一个 Map<String, Any>。因为值是通过键来存取的,所以它不依赖于顺序,非常健壮。

kotlin 复制代码
val CitySaver = mapSaver(
    save = { city ->
        // 保存:将 City 对象拆解成一个 Map
        mapOf("Name" to city.name, "Country" to city.country)
    },
    restore = { map ->
        // 恢复:从 Map 中读取值,重新构建 City 对象
        City(map["Name"] as String, map["Country"] as String)
    }
)

// 使用时,将自定义的 Saver 传递给 stateSaver 参数
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
}
ListSaver - 简洁但需谨慎的选择

ListSaver 将对象转换为一个 List<Any>。它通过索引来存取,代码更简洁,但也更"脆弱"。

kotlin 复制代码
val CitySaver = listSaver<City, Any>(
    save = {
        // 保存:将属性按固定顺序放入列表
        listOf(it.name, it.country)
    },
    restore = {
        // 恢复:按相同顺序从列表中通过索引取值
        City(it[0] as String, it[1] as String)
    }
)

// 使用方法与 MapSaver 相同
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
}
特性 MapSaver ListSaver
工作方式 键值对 (Key-Value) 有序列表 (Index-based)
健壮性 ,不依赖属性顺序 ,顺序改变会导致错误
代码量 略多 非常简洁
推荐场景 大多数自定义对象 属性极少且顺序非常稳定的对象

Part 4: 精准控制:当状态需要"重置"时

rememberrememberSaveable 都会一直持有缓存的值,直到它们离开组合树。但有时我们希望在输入参数变化时,主动重置或重新计算这个状态 。这就是 keyinputs 参数的用武之地。

rememberkey

rememberkey 发生变化时,它会废弃旧的缓存,并重新执行其 lambda 计算块。

kotlin 复制代码
// avatarRes 是一个外部传入的图片资源 ID
val brush = remember(key1 = avatarRes) {
    // 当 avatarRes 改变时,这段代码会重新执行,
    // 创建一个新的、包含新头像的 ShaderBrush
    ShaderBrush(...)
}

rememberSaveableinputs

rememberSaveable 有着同样的能力,参数名为 inputs。当 inputs 变化时,rememberSaveable 会丢弃已保存的状态,并重新执行初始化。

这在"基于外部数据初始化,但允许用户本地修改"的场景下非常有用。

kotlin 复制代码
// typedQuery 是外部传入的搜索词
var userTypedQuery by rememberSaveable(inputs = typedQuery, stateSaver = TextFieldValue.Saver) {
    // 当外部的 typedQuery 改变时,文本框的内容会被重置为这个新值
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

Part 5: 架构与最佳实践 - 状态应该放在哪里?

我们已经掌握了管理状态的工具,但更重要的问题是:状态应该放在哪里?

状态提升 (State Hoisting)

这是一个核心原则:将状态从子 Composable 中"提升"到它们的共同父 Composable 中。这样做的好处是:

  • 单一数据源:状态由一个地方统一管理。
  • 可复用性 :子 Composable 变为无状态 (Stateless) 的,它们只负责接收数据并显示,不关心数据从何而来,因此可以在任何地方复用。
  • 可测试性:无状态的 Composable 更容易进行单元测试。

ViewModel - 屏幕级别状态的最终归宿

对于屏幕级别的状态(那些与业务逻辑紧密相关的状态),最佳实践是将其放在 ViewModel 中。

  • ViewModel 天然具备生命周期感知能力,它能在配置变更后存活,完美解决了状态持久化的问题。
  • 它将业务逻辑与 UI 逻辑分离,使代码结构更清晰。
  • UI (Composable) 通过 StateFlow 或其他可观察类型来订阅 ViewModel 中的状态,并对其做出反应。

结论

掌握状态管理是精通 Jetpack Compose 的关键。让我们快速回顾一下核心工具和使用场景:

  • remember : 用于处理仅与 UI 相关临时性 的状态,以及缓存昂贵的计算
  • rememberSaveable : 用于需要跨越配置变更和进程终止来持久化的状态,如用户输入。
  • @Parcelize, MapSaver, ListSaver : 用于让 rememberSaveable 支持你的自定义数据类型
  • key/inputs : 用于在外部依赖变化时,精准地重置或重新计算被记住的状态。
  • 状态提升 & ViewModel : 遵循这些架构原则,构建可维护、可测试、结构清晰的 Compose 应用。
相关推荐
友人.2273 小时前
Android 底部导航栏 (BottomNavigationView) 制作教程
android
努力学习的小廉4 小时前
初识MYSQL —— 事务
android·mysql·adb
阿里云云原生4 小时前
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
android
.豆鲨包4 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存
JulyYu5 小时前
【Android】针对非SDK接口的限制解决方案
android·客户端
猪哥帅过吴彦祖6 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
2501_916008897 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
stevenzqzq7 小时前
Android Hilt教程_构造函数
android
鹏多多8 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios