引言
Compose作为一个声明式UI框架,如何使用相关组件元素去设计简单的应用界面,之前笔者写过一篇关于初学者将传统的xml迁移到Compose的文章,感兴趣的小伙伴可以看下
但是文章对于Compose如何处理UI中的展示数据,并没有详细展开探讨,一个有灵魂的界面请务必要处理展示用户交互相关的数据,总不能中看不中用吧,与界面相关的数据才是对用户有价值的东西;因此,本文笔者将通过问题的形式,重点解析Compose的数据状态管理(State),探讨Compose如何通过不同的数据源(如Flow、LiveData、RxJava)来高效管理和展示UI数据。带着问题去学习,由浅入深,帮助自己更好地理解和掌握Compose的数据处理机制以及原理,为之后开发更复杂、更动态的应用奠定基础。
OK,话不多说,Let's go
1.什么是Compose中的状态State?
首先在回答这个问题之前,先问一问自己,什么是状态?
对于一个应用APP来说,这个状态指的就是可以随时间变化的任何值。这个定义其实是有点泛的,它包括但不限于数据库,类中的变量,SP持久化存储数据等等
- 网络环境不好时加载数据失败,这时候需要显示个重试按钮或者显示一个Snackbar提示
- 一篇文章点赞数,评论内容,收藏发生变化的时候,需要实时更新显示内容
- 当用户点击按钮时,需要触发相应的动画效果
这些状态的变化需要被及时捕捉和处理,以确保用户体验的流畅和一致。
那么Compose的状态(State)也是如此,作为Compose的核心机制,简单来说,它其实就是数据,亦指UI会随其变化而更新的值,然后这个值呢,可以是任意类型;有时候它非常简单,是一个布尔类型,或者是一个字符串类型。有时候呢,可以是包含整个渲染屏幕的值的复杂数据类型;
2.如何创建状态State?
为了让 Compose 能够感知状态变化,我们需要将状态值包装在一个State对象中。我们使用mutableStateOf() 函数来实现这一点。这将返回一个MutableState 对象,并且 Compose 将跟踪变化并在我们修改值的时候去更新 UI。代码如下所示:
var isChecked by remember { mutableStateOf(true) }
Checkbox(checked = isChecked, onCheckedChange = { isChecked = it })
这里我们创建了一个简单的可变开关状态,咋一看,对初学者来说,是不是有点困惑,没有关系,下面我们一起简单解析下:
- mutableStateOf(true) 将产生一个MutableState 保存我们状态值的对象(即
true
) - remember {} 告诉Compose它需要记住传递给它的值,这样它就不会在每次重组时执行lambda
- by是 Kotlin 中委托的关键字,它隐藏了返回对象的事实MutableState,并允许我们将isChecked当作布尔值来使用
思考一下,如果我们不使用mutableStateOf创建对象,会发生什么?
🙅♂️当然这是不可行的,并且编译器会报错,因为这里我们使用了by关键字使其默认为可变状态对象,这个后面会说到;尽管我们可以在代码中修改isChecked的值,但它是不可变的,isChecked不是一个状态对象,因此无法在 UI 更新时进行重组。
再思考一下,如果不使用by关键字会怎么样?
🙋♂️这当然是可行的,只是写法上会有小小的变化,此时isChecked将持有的是MutableState 的引用,这个时候的isChecked就不是布尔值了,为了更新或修改状态,你需要使用state.value来访问它的值
再再思考一下,如果我不用remember又会怎么样?
🙅♂️这当然是不可行的,如果只使用 mutableStateOf而不结合 remember可能会导致状态在重组过程中被重置,进而导致应用行为不如预期。这里的 mutableStateOf 是在每次重组时被重新初始化的,这意味着每次重组时 isChecked都会被重置为 false。这是因为每次重组都会重新执行这个代码块,而不是保留之前的状态。
3.有状态Stateful和无状态Stateless有什么区别?
话不多说,我们先通过表格形式对比下有状态和无状态的区别
特性 | 有状态(Stateful)组件 | 无状态(Stateful)组件 |
---|---|---|
状态管理 | 内部管理自己的状态 | 不管理状态,依赖外部传递的状态 |
状态更新 | 内部处理状态更新 | 通过回调函数通知外部组件进行状态更新 |
适用场景 | 需要直接处理和响应用户交互的组件 | 通用的、可重用的组件 |
复杂性 | 通常更复杂,包含状态逻辑 | 通常更简单,关注于渲染 UI |
什么不够清晰,那下面我们举个🌰,假设有一个需要管理计数的应用,我们用分别用有状态的组件和无状态的组件去实现
有状态 (Stateful) 组件
有状态组件是指内部管理自己的状态并根据状态的变化来更新 UI 的组件,这些组件包含并维护状态。
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun StatefulCounter() {
// 使用 remember 来创建和记住一个可变的状态
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
@Preview
@Composable
fun PreviewStatefulCounter() {
StatefulCounter()
}
无状态 (Stateless) 组件
无状态组件不维护自己的状态。它们依赖于外部传递的参数来呈现 UI。它们通常用于封装逻辑和呈现内容,并通过回调函数通知外部组件状态的变化。
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = onIncrement) {
Text("Increment")
}
}
}
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count = count, onIncrement = { count++ })
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- StatelessCounter 是一个无状态组件,它通过 count参数显示计数,并通过 onIncrement 回调通知状态变化。
- CounterScreen组件维护 count 状态,并将状态和回调函数传递给 StatelessCounter。
通过这种分离,让我们的代码变得更模块化、更易于测试和维护。
4.什么是状态提升(State Hoisting)?
状态提升(State Hoisting)是指将组件的状态提升到其父组件中管理,而不是由子组件自身管理。这样做可以使得多个子组件共享状态,并通过父组件的状态控制子组件的行为。这种模式在需要多个组件之间共享状态或从外部控制组件时特别有用。
一般对于状态提升有以下基本模式处理
- 在父组件中管理状态
- 通过参数将状态传递给子组件
- 通过回调函数将状态更新逻辑传递给子组件
你说什么,还是不够清晰,那我们在上面管理计数的🌰基础上扩展下,这里需要两个组件共享和更新计数。
开始状态提升
@Composable
fun CounterScreen() {
// 在父组件中管理状态
var count by remember { mutableStateOf(0) }
Column(modifier = Modifier.padding(16.dp)) {
// 将状态和状态更新函数传递给子组件
StatelessCounter(count = count, onIncrement = { count++ })
Spacer(modifier = Modifier.height(8.dp))
StatelessCounter(count = count, onIncrement = { count++ })
}
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- CounterScreen 作为父组件,它管理 count 的状态。
- StatelessCounter 是无状态组件,它通过 count 参数显示计数,并通过 onIncrement 回调通知父组件更新计数。
- 两个无状态组件共享同一个状态,并且通过状态提升可以同步更新。
状态提升使得多个组件能够共享和同步状态,同时保持单向数据流。它提高了代码的可维护性和可测试性,是构建复杂 UI 组件时的一种推荐模式
5. 为什么要使用状态提升?
承接上题,我们用一个表格来直观总结下:
优点 | 说明 |
---|---|
单向数据流 | 保持单向数据流,使得数据流向更加清晰,组件之间的依赖关系更加明确 |
更好的可测试性 | 状态提升使得组件变得更加可预测和易于测试,因为状态逻辑集中在一个地方 |
共享状态 | 当多个组件需要共享同一个状态时,可以将状态提升到它们的共同父组件 |
ok,下面我们分别用伪代码来稍微展开下状态提升的重要性
1.单向数据流
假设我们有一个父Composable和两个子Composable。子ComposableA会影响子Composable B的显示,通过单向数据流,状态提升到父Composable中
@Composable
fun Parent() {
var text by remember { mutableStateOf("") }
Column {
ChildA(text) { newText -> text = newText }
ChildB(text)
}
}
@Composable
fun ChildA(text: String, onTextChange: (String) -> Unit) {
TextField(
value = text,
onValueChange = onTextChange,
label = { Text("Enter text") }
)
}
@Composable
fun ChildB(text: String) {
Text(text = "You entered: $text")
}
上面例子中,Parent管理状态,ChildA负责修改状态,ChildB负责展示状态,这使得数据流向清晰,便于调试。
2.更好的可测试性
我们继续上面的例子,我们可以更容易地测试Parent Composeable的状态管理
// 假设我们使用Jetpack Compose的测试库进行测试
@Test
fun testParentComposable() {
composeTestRule.setContent {
Parent()
}
// 输入新的文本
composeTestRule.onNodeWithTag("textField").performTextInput("new value")
// 检查Text composable显示的文本是否正确
composeTestRule.onNodeWithText("You entered: new value").assertExists()
}
3.共享状态
假设有两个子Composable A和B需要共享一个计数器状态
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
Column {
ChildA(count) { count++ }
ChildB(count) { count-- }
}
}
@Composable
fun ChildA(count: Int, increment: () -> Unit) {
Column {
Text(text = "Count: $count")
Button(onClick = increment) {
Text("Increment")
}
}
}
@Composable
fun ChildB(count: Int, decrement: () -> Unit) {
Column {
Text(text = "Count: $count")
Button(onClick = decrement) {
Text("Decrement")
}
}
}
这个例子中,Parent Composable管理count状态,ChildA和ChildB共享这个状态并分别提供增减功能。通过提升状态,我们可以确保两个Composable都能访问和更新同一个状态,避免数据不一致的问题。
此外 ,大家可以思考下,有时候真的需要状态提升嘛?
并不是所有场景都适合状态提升,具体业务需求具体分析,有时候界面元素不需要组合的时候,比如说我有一个可以展开收起的文本,它是独立的,这时候将状态和元素写到同一个函数当中是可以接受的,这样看起来也非常简单直接,相反如果强行去提升状态的话,反而看起来有些冗余。那官方的术语总结下,就是如果你的应用的状态逻辑很简单,并且在当前界面其他部分不需要这个状态,基本独立的,就不需要状态提升(比如说动画状态)
@Composable
fun ChatBubble(
message: Message
) {
//展示详情的状态
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
ok,总结下,之所以使用状态提升,进行单向数据流管理和共享多个状态,就是让我们的代码结构更加清晰、可预测和易于测试。
6. 如何与ViewModel结合管理状态State?
ViewModel 可以存储和管理与 UI 相关的数据,那么Compose与ViewModel结合使用,它有助于将 UI 状态与业务逻辑分离,并且能很好地处理配置更改(例如屏幕旋转等),这时候有同学就有疑问了,那万一我没有ViewModel的话,如何确保在屏幕旋转下保持状态呢? 我知道你很急,但你先别急,这个问题文章后面会有解答,下面先让我们来看看ViewModel结合State需要哪些步骤, 如下图所示
ok,用个简单的例子来辅助说明下吧,对,还是那个熟悉的配方,计数的应用示例,给小弟我抬上来把。我们继续修改下,看看如何结合ViewModel进行使用
1. 创建 ViewModel
首先,创建一个ViewModel类,并在其中定义需要管理的状态。
class CounterViewModel : ViewModel() {
// 内部状态
private val _count = mutableStateOf(0)
// 对外公开的不可变状态
val count: State<Int> get() = _count
// 增加计数的方法
fun increment() {
_count.value++
}
}
- CounterViewModel定义了一个私有的可变状态 _count 和一个对外公开的不可变状态 count。
- increment 方法用于更新状态。
2. 在 Composable 函数中使用 ViewModel
使用 viewModel() 函数在 Composable函数 中获取ViewModel实例,并通过观察ViewModel中的状态来更新UI。
@Composable
fun CounterScreen(counterViewModel: CounterViewModel = viewModel()) {
// 从 ViewModel 中获取状态
val count by counterViewModel.count
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { counterViewModel.increment() }) {
Text("Increment")
}
}
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- 使用 viewModel() 获取ViewModel 实例。如果未提供参数,默认会使用由 ViewModelProvider 提供的 ViewModel。
- val count by counterViewModel.count通过解构方式获取 count的当前值,使得当 count改变时,UI 会自动重组。
- Button 的onClick 回调调用 counterViewModel.increment() 更新状态。
优点 | 说明 |
---|---|
持久化状态 | ViewModel 能在配置更改(如屏幕旋转)时保持状态不变。 |
分离关注点 | 将业务逻辑和 UI 状态管理分离到 ViewModel 中,使 Composable 函数更加简洁和可读。 |
更好的测试 | ViewModel 更容易进行单元测试,因为它不依赖于 Android 框架组件。 |
7.如何在Compose中使用Flow管理状态?
对于Flow,小伙伴们应该非常熟悉了吧,它在处理异步的数据流的时候是非常高效的,在结合ViewModel的基础下,使用 Flow 从 ViewModel 中提供数据,并在 Composable函数中观察和响应这些数据的变化。
不置可否,熟悉的计数应用它又来了,这里我们将原来计数的方法用Flow去替代,一起来瞧瞧该怎么改
1.在原来的ViewModel中去创建Flow,然后给它暴露出去
class CounterViewModel : ViewModel() {
// //内部状态
// private val _count = mutableStateOf(0)
// // 对外公开的不可变状态
// val count: State<Int> get() = _count
// 定义一个 MutableStateFlow 来管理内部状态
private val _count = MutableStateFlow(0)
// 暴露一个不可变的 StateFlow
val count: StateFlow<Int> = _count.asStateFlow()
//计数
fun increment() {
viewModelScope.launch {
_count.value++
}
}
}
- MutableStateFlow 用于管理内部状态。
- StateFlow 用于暴露不可变的状态流。
- increment方法通过 viewModelScope.launch 更新 _count 的值。
2. 在 Composable 函数中收集 Flow
重点来了,使用 collectAsState 在 Composable 中收集 Flow 并更新 UI。
@Composable
fun CounterScreen(counterViewModel: CounterViewModel = viewModel()) {
//ViewModel中获取状态
// val count by counterViewModel.count
//收集 ViewModel 中的 StateFlow 并转换为 Compose 状态
val count by counterViewModel.count.collectAsState()
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { counterViewModel.increment() }) {
Text("Increment")
}
}
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- 使用 collectAsState 将 Flow 转换为 Compose 状态。
- 使用by 委托将Flow 的最新值赋值给 count。
优点 | 说明 |
---|---|
响应式编程 | Flow 使得你能够处理异步数据流,并响应数据变化 |
生命周期感知 | 使用 collectAsState 是生命周期感知的,确保 Flow 只在 Composable 处于活动状态时收集 |
清晰的状态管理 | 通过 Flow,你可以更清晰地管理和处理状态变化,特别是在处理异步操作时 |
这种结合State使用Flow的模式,可以帮助你构建响应式和高效的UI,特别适用于需要处理实时数据或异步任务的场景。
8.如何在Compose中使用LiveData管理状态?
LiveData 是一种生命周期感知的可观察数据持有者,它可以让 Compose 组件观察数据变化,并在数据变化时自动重组界面。下面,我们看看具体该怎么用把,如图所示
来吧,还是把计数应用的🌰请上来,这次我们使用LiveData来实现计数功能
1. 在 ViewModel 中创建和暴露 LiveData
首先,在我们ViewModel中定义 LiveData
class CounterViewModel : ViewModel() {
// //内部状态
// private val _count = mutableStateOf(0)
// // 对外公开的不可变状态
// val count: State<Int> get() = _count
// // 定义一个 MutableStateFlow 来管理内部状态
// private val _count = MutableStateFlow(0)
// // 暴露一个不可变的 StateFlow
// val count: StateFlow<Int> = _count.asStateFlow()
// 内部 MutableLiveData,用于管理计数状态
private val _count = MutableLiveData(0)
// 暴露不可变的 LiveData
val count: LiveData<Int> = _count
//计数
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
- MutableLiveData 用于管理内部状态 _count。
- LiveData 用于暴露不可变的状态 count。
- increment 方法用于更新 _count 的值。
2. 在 Composable 中观察 LiveData
@Composable
fun CounterScreen(counterViewModel: CounterViewModel = viewModel()) {
//ViewModel中获取状态
// val count by counterViewModel.count
// //收集 ViewModel 中的 StateFlow 并转换为 Compose 状态
// val count by counterViewModel.count.collectAsState()
// 使用 observeAsState 将 LiveData 转换为 Compose 状态
val count by counterViewModel.count.observeAsState(0)
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { counterViewModel.increment() }) {
Text("Increment")
}
}
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- 主要使用 observeAsState 将LiveData 转换为 Compose 的 State。
- 使用 by 委托将LiveData的最新值赋值给 count。
优点 | 说明 |
---|---|
生命周期感知 | LiveData 是生命周期感知的,确保数据只在 Composable 处于活动状态时被观察 |
现有代码集成 | 如果你的项目已经使用了 LiveData,可以轻松地将其与 Compose 集成,而无需完全重写状态管理逻辑 |
简洁的状态管理 | 通过 LiveData,你可以清晰地管理和处理状态变化,并自动响应数据变化 |
9.如何在Compose中使用RxJava管理状态?
看到这个问题,有同学就会说了,都使用协程了,还需要在Compose中使用Rxjava吗,这不是徒增烦恼嘛。这个只能说,看个人业务需求,Compose是支持Rxjava进行管理状态的,具体那些该不该用,好不好用这里就不做扩展讨论,反正就笔者个人而言,在Compose项目开发中基本上是用不到Rxjava的。下面,我们看看具体该怎么用吧,如图所示
依旧是在计数的应用示例上进行修改,要记得添加下依赖哦
// RxJava and RxAndroid dependencies
implementation "io.reactivex.rxjava3:rxjava:3.1.5"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
1. 在 ViewModel 中创建和暴露 Observable/Flowable
首先,在我们的ViewModel中定义一个 PublishSubject 或其他类型的RxJava流来管理状态。
class CounterViewModel : ViewModel() {
// //内部状态
// private val _count = mutableStateOf(0)
// // 对外公开的不可变状态
// val count: State<Int> get() = _count
// // 定义一个 MutableStateFlow 来管理内部状态
// private val _count = MutableStateFlow(0)
// // 暴露一个不可变的 StateFlow
// val count: StateFlow<Int> = _count.asStateFlow()
// // 内部 MutableLiveData,用于管理计数状态
// private val _count = MutableLiveData(0)
// // 暴露不可变的 LiveData
// val count: LiveData<Int> = _count
// 创建一个 PublishSubject 来管理计数状态
private val _countSubject = PublishSubject.create<Int>()
// 暴露一个 Observable
val countObservable: Observable<Int> = _countSubject.startWithItem(0)
private var currentCount = 0
//计数
fun increment() {
currentCount++
_countSubject.onNext(currentCount)
}
}
- PublishSubject 用于管理内部状态。
- countObservable暴露一个 Observable出去,并通过 startWithItem(0) 初始化为 0。
- increment方法更新当前计数并通过 onNext 将其发送给观察者。
2. 在 Composable 中收集 Observable/Flowable
@Composable
fun CounterScreen(counterViewModel: CounterViewModel = viewModel()) {
//ViewModel中获取状态
// val count by counterViewModel.count
// //收集 ViewModel 中的 StateFlow 并转换为 Compose 状态
// val count by counterViewModel.count.collectAsState()
// 使用 observeAsState 将 LiveData 转换为 Compose 状态
// val count by counterViewModel.count.observeAsState(0)
val disposables = remember { CompositeDisposable() }
var count by remember { mutableStateOf(0) }
DisposableEffect(key1 = Unit, effect = {
// 订阅 Observable 并将其结果转换为 Compose 状态
val disposable = counterViewModel.countObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
count = it
}, {
//todo 处理错误
})
disposables.add(disposable)
onDispose {
disposables.clear()
}
})
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Count: $count")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { counterViewModel.increment() }) {
Text("Increment")
}
}
}
@Preview
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}
- 使用 remember和 DisposableEffect 管理RxJava订阅的生命周期。
- 使用collectAsState将Observable 转换为 Compose 的状态。
- DisposableEffect 确保在 Composable 销毁时清理订阅,避免内存泄漏。
尽管Rxjava在目前Compose中几乎不怎么使用了,但它极其强大,是独立与Kotlin的第三方库,不当当局限于这个领域,这里就不继续扯皮了,这不是重点,略过略过。
优点 | 说明 |
---|---|
响应式编程 | RxJava 允许处理复杂的异步流和组合操作 |
清晰的状态管理 | 通过 Observable,我们可以清晰地管理和处理状态变化。 |
强大的功能 | RxJava 提供了丰富的操作符,可以轻松处理复杂的数据流。 |
10.如何确保状态在设备旋转等情况下保持?
Compose中如果要确保状态在设备旋转配置发生变化下保持,一般可以通过ViewModel和rememberSaveable两种方式来实现。
前者这里就不过多赘述了,相信各位同学已经非常熟悉了,我们主要来讨论下后者,rememberSaveable 是一个用于保存状态的Compose API,它可以自动保存状态,并在配置更改(如设备旋转)时恢复这些状态。它使用 SavedStateRegistry进行状态保存,可以简单看下源码
@Composable
fun <T> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T {
val savedStateRegistry = LocalSavedStateRegistryOwner.current.savedStateRegistry
val savedStateProvider = remember { SaveableStateRegistry(savedStateRegistry) }
val (value, onValueChanged) = rememberSaveableState(key, init, saver, savedStateProvider)
DisposableEffect(savedStateRegistry) {
val registryEntry = savedStateProvider.registerSavedStateProvider(key ?: value.hashCode().toString()) {
mapOf(SAVEABLE_KEY to saver.save(value))
}
onDispose {
registryEntry.unregister()
}
}
return value
}
下面我们简单梳理下上述这段代码
首先去获取 SavedStateRegistry:
- 通过 LocalSavedStateRegistryOwner 获取当前的SavedStateRegistry 实例,这个实例用于在配置更改时保存和恢复状态。
创建SaveableStateRegistry:
- SaveableStateRegistry 包装了 SavedStateRegistry 并提供了 registerSavedStateProvider 和 unregisterSavedStateProvider 方法来管理状态提供者。
保存和恢复状态:
- rememberSaveableState 函数通过 key 来唯一标识保存的状态。首先尝试从 SavedStateRegistry 中恢复状态,如果恢复失败则使用 init 创建初始状态。
- 使用 Saver 将状态保存为可以持久化的数据,并通过 SavedStateRegistry 注册。
DisposableEffect:
- DisposableEffect 确保在Composable结束时清理状态提供者,以避免内存泄漏。
简单来说,rememberSaveable 在Compose中通过使用 SavedStateRegistry 来保存和恢复状态,使状态在配置更改(如设备旋转)时得以保留,如果想要深入了解的同学可以详情看下Compose相关源码和官方文档。
the last,可以跟着谷歌官方的Jetpack Compose clonelabs学习课程,本文中的例子就是取材其中进行扩展的 ,2024共勉