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 应用。
相关推荐
Digitally1 小时前
如何将照片从vivo手机传输到电脑
android
小兔薯了10 小时前
7. LNMP-wordpress
android·运维·服务器·数据库·nginx·php
L***d67010 小时前
mysql的主从配置
android·mysql·adb
Sammyyyyy12 小时前
PHP 8.5 新特性:10 大核心改进
android·php·android studio
TO_ZRG12 小时前
Unity 通过 NativePlugin 接入Android SDK 指南
android·unity·游戏引擎
n***840712 小时前
Springboot-配置文件中敏感信息的加密:三种加密保护方法比较
android·前端·后端
方白羽13 小时前
一次由 by lazy 引发的“数据倒灌”,深入理解 `by`关键字、`lazy`函数的本质
android·kotlin·app
v***553414 小时前
MySQL 中如何进行 SQL 调优
android·sql·mysql
vx_vxbs6615 小时前
【SSM高校普法系统】(免费领源码+演示录像)|可做计算机毕设Java、Python、PHP、小程序APP、C#、爬虫大数据、单片机、文案
android·java·python·mysql·小程序·php·idea
j***827016 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
android·前端·后端