Android Compose 状态保存的API总结

文章目录

  • [remember 最基础的状态保存(抵抗重组)](#remember 最基础的状态保存(抵抗重组))
  • [rememberSaveable 重建 activity 或进程之后保留状态 (生命周期持久)](#rememberSaveable 重建 activity 或进程之后保留状态 (生命周期持久))
  • [ViewModel 状态容器](#ViewModel 状态容器)
    • [ViewModel 中如何声明状态能被 compose 使用?](#ViewModel 中如何声明状态能被 compose 使用?)
    • [StateFlow 状态(数据)流 (用于ViewModel中)](#StateFlow 状态(数据)流 (用于ViewModel中))
    • [SavedStateHandle 键值对保存,生命周期持久 (用于ViewModel中)](#SavedStateHandle 键值对保存,生命周期持久 (用于ViewModel中))
  • [状态保存 API 对比总结](#状态保存 API 对比总结)
    • [状态持久化中,`Bundle` 机制做了什么?](#状态持久化中,Bundle 机制做了什么?)
    • [rememberSaveable 中的 `Bundle` 机制](#rememberSaveable 中的 Bundle 机制)
    • [SavedStateHandle 中的 `Bundle` 机制](#SavedStateHandle 中的 Bundle 机制)
  • [组合使用 状态保存 api 示例](#组合使用 状态保存 api 示例)
  • [ViewModel 的生命周期](#ViewModel 的生命周期)

在上一篇《Android Compose 状态:核心api,状态恢复,状态提升,状态容器》 提过,使用 rememberrememberSaveable api 和 ViewModel 可以进行状态保存。

还有 StateFlowSavedStateHandle 也可以进行状态保存。

但它们作用的层级、生命周期和解决的问题各有不同


remember 最基础的状态保存(抵抗重组)

remember {} 是最基础的 状态保存 api。remember 的核心作用是让状态在"重组"期间存活下来。

val xx by remember { mutableStateOf(...) } 到底在保存什么? mutableStateOf 是创建一个观察的状态,这个状态值变了,会通知所有读取它的 composable 重新执行,即重组。

如果没有 remember ,那每次重组,都是重新赋值成初始值。

加上了 remember,在每次状态值改变时,发生内存缓存,后续每次重组,都是先判断从缓存中获取到最新值。

比如,animateColorAsState ,用于在两个颜色值之间实现平滑的过渡动画。内部以 remember 实现

声明时,不使用 rememberval backColorState by animateColorAsState(...)


rememberSaveable 重建 activity 或进程之后保留状态 (生命周期持久)

  • 重建activity的场景,如配置更改(例如:屏幕旋转、切换深浅色模式、改变语言)
  • 重建进程的场景,系统杀死了后台进程

rememberSaveable 不仅能抵抗重组,还能抵抗配置更改和进程被杀

它支持可存入 Bundle对象的 任意数据类型;对于其它数据类型,需要进行序列化,或自定义 saver。

有些状态,就是不适合持久化保存的,它们是"极其短暂的、瞬时的 UI 状态"或"纯内存引用

例如:一个按钮按下时的水波纹动画效果、一个极其短暂的防抖动时间戳。 为了这些东西去序列化存入 Bundle,反而会浪费系统性能。

这些状态丢了就丢了,完全不影响用户体验。

官方的 可组合函数库中,大量提供了 rememberXxxState 的状态,它们的底层大多数使用了 rememberSaveable。

例如:

val scrollState = rememberScrollState() // 滚动视图状态

val listState = rememberLazyListState() // 用于延迟加载列表组件

val pagerState = rememberPagerState(pageCount = { 10 }) // 用于 ViewPager 效果的轮播图或翻页组件

val snackbarHostState = remember { SnackbarHostState() } // 用于控制 Material Design 中的底部轻量提示

val sheetState = rememberModalBottomSheetState() // 用于控制从屏幕底部弹出的模态底板

...

比如,使用 androidx.compose.material3.DatePicker,发现还提供了专属的状态对象 DatePickerState,其doc注释,提示了 状态保存函数:rememberDatePickerState

该函数内就是 以 rememberSaveable 实现的。向 它传入了自定义的 saver 对象 DatePickerStateImpl.Saver ,继续跟踪查看,该 save 是使用 listSaver 实现的。


ViewModel 状态容器

用于管理 业务逻辑 和 屏幕级别的状态

其生命周期比普通的 Composable 更长,能够在使用配置更改(如屏幕旋转)时存活下来。

可声明持有多种 数据状态的引用。

可提供业务逻辑控制方法;可在方法中开启协程。

ViewModel 中如何声明状态能被 compose 使用?

  • State<T> 类型

使用 mutableStateOf api 包装

kotlin 复制代码
class UserViewModel : ViewModel() {
    // 暴露给 Compose 的屏幕状态
    val userName by mutableStateOf("")
}
  • StateFlow 状态(数据)流
  • SavedStateHandle 键值对保存,生命周期持久

StateFlow 状态(数据)流 (用于ViewModel中)

StateFlow 是 Kotlin 协程库(Coroutines)提供的一种状态持有可观察数据流。它始终有且只有一个最新值。当它的值发生变化时,它会自动通知所有的观察者。它通常被用在 ViewModel 中,用于向 UI 层(Composable)暴露业务数据或UI状态。

特点:

  • 只要 ViewModel 存活(例如屏幕旋转时),它包含的数据就不会丢失。
  • 但它默认无法在"系统级进程被杀(Process Death)"后存活。

示例:

kotlin 复制代码
class UserViewModel : ViewModel() {
    // 内部可变的状态
    private val _userName = MutableStateFlow("默认用户")
    // 暴露给外部的不可变状态
    val userName = _userName.asStateFlow()

    fun updateName(newName: String) {
        _userName.value = newName
    }
}

// 在 Compose 中使用
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    // 推荐使用 collectAsStateWithLifecycle() 感知生命周期收集
    val userName by viewModel.userName.collectAsStateWithLifecycle()

    Column {
        Text("当前用户: $userName")
        Button(onClick = { viewModel.updateName("张三") }) {
            Text("修改名字")
        }
    }
}

SavedStateHandle 键值对保存,生命周期持久 (用于ViewModel中)

ViewModel 在系统由于内存不足杀死应用进程后,能够保存和恢复状态

使用时, SavedStateHandle 类型参数要作为 ViewModel构造函数参数

  • 使用 SavedStateHandle#saveable api ,配合 mutableStateOf 实现 读取和写入界面元素状态。
kotlin 复制代码
class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
	 // 状态声明委托
    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput() {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

saveable API 开箱就支持基元类型,并会收到 stateSaver 参数,以便使用自定义 Saver(就像 rememberSaveable() 一样)。

  • 使用键值对 api
kotlin 复制代码
class SearchViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 定义一个键
    private val SEARCH_QUERY_KEY = "search_query"

    // getStateFlow 会自动从 SavedStateHandle 中读取历史值,如果没有则使用初始值 ""
    // 当 SavedStateHandle[SEARCH_QUERY_KEY] 被修改时,这个 StateFlow 会自动更新
    val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY_KEY, "")

    fun updateSearchQuery(query: String) {
        // 更新键值对,会自动触发上方的 getStateFlow 更新
        savedStateHandle[SEARCH_QUERY_KEY] = query
    }
}

@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    val query by viewModel.searchQuery.collectAsStateWithLifecycle()
    
    TextField(
        value = query,
        onValueChange = { viewModel.updateSearchQuery(it) },
        label = { Text("搜索") }
    )
}

状态保存 API 对比总结

  • remember
    用在 composable 函数中 ,在重组后保持最新值

    侧重于 瞬时UI 状态

  • rememberSaveable (生命周期持久)
    用在 composable 函数中 ,不光能在重组后保持最新值,且当配置更改或进程被杀死导致进程重建、Activity重建,compose ui 重新渲染时,也能保持最新值

    底层存储依赖 Bundle 机制

    侧重于交互状态

  • StateFlow
    用在 ViewModel 中 ,生命周期依赖 ViewModel 的 生命周期

    普通的业务数据 / 大体量数据 / 需要复杂处理转换的数据 / 不能或不方便序列化的数据

  • SavedStateHandle
    用在 ViewModel 中 ,键值对保存,生命周期持久

    底层存储依赖 Bundle 机制

    侧重于核心业务数据状态

状态持久化中,Bundle 机制做了什么?

rememberSaveable 中的 Bundle 机制

  • 核心组件:SaveableStateRegistry(保存注册表)
    rememberSaveable 并不直接操作 Bundle。在 Compose 层,有一个名为 SaveableStateRegistry 的接口。

在 Composable 中调用 rememberSaveable 时,它会产生一个唯一的 Key,并将状态值(以及如何保存它的逻辑)注册到这个注册表中。

这个注册表的作用是:在 Composable 存续期间,它像普通的 remember 一样在内存里维护数据;但在系统准备回收资源时,它负责把数据"打包"。

  • Saver(序列化器)
    Bundle 只能存储特定的数据类型(如 String, Int, Parcelable 等)。如果你的状态是一个复杂的自定义对象(比如一个 Data Class),Bundle 存不下。

这时候 Saver 就发挥作用了:

Save (保存):将复杂的对象拆解成 Bundle 能接受的简单类型(序列化)。

Restore (恢复):将从 Bundle 读出的简单类型重新构造成复杂的对象(反序列化)。

注:如果你存的是 String 或 Int 等基础类型,Compose 会提供默认的 Saver。

  • 保存流程:从内存到系统进程
    当系统因为内存压力准备杀死进程,或者发生配置更改(如旋转屏幕)时:

触发回调:Activity 的 onSaveInstanceState(outState: Bundle) 被触发。

收集数据:Compose 框架会调用 SaveableStateRegistry 的保存方法,把所有注册过的 rememberSaveable 状态通过 Saver 转换成键值对。

这些键值对被存入 Activity 的 outState 这个大 Bundle 中。

这个 Bundle 最终会被传递给系统进程(System Server)托管,它不随 App 进程的销毁而消失。

  • 恢复流程:从系统进程回到 Composable
    当进程重建,Activity 重新启动时:

分发数据:系统将托管的 Bundle 传回给新 Activity 的 onCreate。

重建注册表:Compose 框架根据这个 Bundle 重新构建 SaveableStateRegistry。

重新绑定:当 Composable 函数再次运行到 rememberSaveable 这一行代码时:

它会根据其位置信息或手动指定的 Key 去注册表里查找。

如果找到了之前保存的值,就通过 Saver 的 restore 方法把它变回原来的对象。

如果没找到则初始化。

SavedStateHandle 中的 Bundle 机制

  • 进程被杀前(数据保存):

    当系统决定回收你的 App 进程时,Activity 会触发 onSaveInstanceState。此时, SavedStateHandle 会将其内部持有的数据序列化到一个 Bundle 中,这个 Bundle 会被交给系统进程托管,它不随 App 进程的销毁而消失。

  • 进程重建时(数据恢复):

    系统进程会将之前保存的 Bundle 传回给新创建的 Activity 进程。

  • ViewModel 创建阶段:

    当创建 ViewModel 实例时,Compose 或 Activity 默认使用的 SavedStateViewModelFactory 会介入。它会从系统的 Bundle 中提取出属于该 ViewModel 的那部分数据,并用这些数据构造出一个 SavedStateHandle。

  • 注入:

    Factory 将这个"装满了旧数据"的 SavedStateHandle 传入 ViewModel 的构造函数。


组合使用 状态保存 api 示例

示例功能:默认加载文章列表;从文章列表点击后,进入文章详情,并根据 文章id 加载详情数据

根据状态变化的自定义页面路由:

kotlin 复制代码
// 定义页面枚举或密封类
sealed class Screen {
    object List : Screen()
    data class Detail(val articleId: String) : Screen()
}

@Composable
fun MyApp() {
    // 维护当前的页面状态, 默认是 列表页面 
    var currentScreen by remember { mutableStateOf<Screen>(Screen.List) }

    // 根据状态决定渲染哪个页面
    when (val screen = currentScreen) {
        is Screen.List -> {
            ArticleListScreen(
                onArticleClick = { id ->
                    // 切换状态,触发重组,UI 就会"跳"到详情页
                    currentScreen = Screen.Detail(id)
                }
            )
        }
        is Screen.Detail -> {
            ArticleScreen(
                articleId = screen.articleId,
                onBack = { currentScreen = Screen.List } // 提供返回逻辑
            )
        }
    }
}

文章列表页的 可组合函数,(省略 ArticleListViewModel )

kotlin 复制代码
@Composable()
fun ArticleListScreen(viewModel: ArticleListViewModel = viewModel(), onArticleClick: (String) -> Unit) {
	
	val list: List<ArticleSimpleData> = ... // 从vm获取 stateFlow 文章列表
	LazyColumn { // 列表
		items(list) { item ->
			Box(modifier = Modifier.clickable { 
				onArticleClick(item.id)
			})
		}
	}
}

文章详情页的 ViewModel 和 可组合函数:

kotlin 复制代码
// 逻辑层:ViewModel 负责业务和核心状态恢复
class ArticleViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // SavedStateHandle 保存核心状态: 文章ID
    private val articleId = savedStateHandle.getStateFlow("ARTICLE_ID", "")

    // StateFlow 负责暴露庞大/复杂的业务数据
    val articleContent: StateFlow<Article> = articleId.flatMapLatest { id ->
        repository.fetchArticle(id) // 从网络或数据库获取大体量数据
    }.stateIn(viewModelScope, SharingStarted.Lazily, Article.Empty)
	
	fun updateArticleId(id: String) {
		savedStateHandle["ARTICLE_ID"] = id
	}
}

@Composable
fun ArticleScreen(viewModel: ArticleViewModel = viewModel(), articleId: String, onBack: () -> Unit) {
    // 观察业务逻辑状态
    val article by viewModel.articleContent.collectAsStateWithLifecycle()

    // rememberSaveable 负责纯 UI 的状态(比如用户是否点击了"点赞"的临时动画状态,或者阅读到的滚动位置)
    var isImageZoomed by rememberSaveable { mutableStateOf(false) }
		
	// 动画:当 isImageZoomed 改变时,scale 会在 1f 和 2.5f 之间平滑过渡
    val scale by animateFloatAsState(
        targetValue = if (isImageZoomed) 2.5f else 1f,
        // 自定义动画规格(增加一点回弹效果)
        animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
        label = "ImageZoomAnimation"
    )

	// 文章详情
    Column {
        Text(article.title)
        Image(
            // ...
            modifier = Modifier
            	// 图层缩放,跳过布局阶段,只在绘制阶段生效
            	.graphicsLayer(
            		scaleX = scale,
            		scaleY = scale
           		)
           		.clickable { isImageZoomed = !isImageZoomed }
        )
    }
	
	// LaunchedEffect 会在 Composable 首次显示或 articleId 改变时执行
    LaunchedEffect(articleId) {
        viewModel.updateArticleId(articleId)
    }

}

ViewModel 的生命周期

ViewModel 的生命周期并不默认与某个具体的 Composable 函数绑定,它的生命周期级别取决于它被存储在哪个 ViewModelStoreOwner

  • 默认行为:Activity 级别

直接在 Composable 中调用 viewModel()(来自 androidx.lifecycle.viewmodel.compose)时,它底层会去寻找最近的 ViewModelStoreOwner。

在没有使用官方 Navigation 库的情况下,这个 Owner 通常就是所在的ComponentActivity

如上示例 ArticleViewModel、ArticleListViewModel 本质上都是 相对于所在 Activity 的单例对象

  • 使用 Navigation 库时:Destination 页面级别

使用了 androidx.navigation.compose,情况会有所不同。

机制:导航图中的每一个"目的地"(Destination)都会被包装成一个 NavBackStackEntry,而这个 Entry 本身实现了 ViewModelStoreOwner

结果:此时在 Composable 中调用 viewModel(),它获取的是绑定在当前"页面路由"上的实例。当用户点击返回键,该路由从回退栈弹出时,对应的 ViewModel 才会执行 onCleared()。

级别:这种情况下,它的级别比 Activity 低,是"页面级"的

相关推荐
BLUcoding2 小时前
Android 生命周期详解
android
Swift社区2 小时前
鸿蒙 vs iOS / Android:谁更适合 AI?
android·ios·harmonyos
冬奇Lab3 小时前
硬件加速与 OMX/Codec2:解密编解码器的底层世界
android·音视频开发·视频编码
亘元有量-流量变现3 小时前
ASO优化全流程实操指南:从基础到迭代,精准提升App曝光与转化
android·ios·harmonyos·aso优化·方糖试玩
私人珍藏库4 小时前
【Android】GameNative 0.9.0 [特殊字符] 手机畅玩Steam游戏
android·游戏·智能手机·app·工具·软件·多功能
诸神黄昏EX4 小时前
Android Safety 系列专题【篇七:Android AVF机制】
android
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.4 小时前
MySQL 主从架构中的使用技巧及优化
android·mysql·架构
羊小蜜.4 小时前
Mysql 11: 存储过程全解——从创建到使用
android·数据库·mysql·存储过程
zh_xuan4 小时前
Android compose和传统view混用
android