☰
Compose 基础 - 状态 State
2026/9/26 9:59:31 网站建设 项目流程

参考文章1

参考文章2

官方页面

一、概念

Compose将界面(可组合函数)和数据(状态)分开,事件会引起状态的变化,进而使读取了该状态的组件会发生重组。

MutableState 会与读取它的组合函数绑定,当所持有的状态(值)发生变化时通知组件进行界面刷新(重组)。

remember()、rememberSaveable()、retain() 提供持久化存储功能,因为使用了纯函数形式表达界面,如果在函数中声明属性(局部变量),会因为函数被多次调用而丢失状态(值)。

二、MutableState

Compose仅对“可观察”的状态变化做出响应,普通变量变化无法感知不会触发重组。MutableState 是 Compose 专用的观察者模式的容器(同LiveData、StateFlow一样可以观察到值的变化),会与订阅它的组件绑定,当所持有的值发生变化时通知组件进行重组刷新。

MutableState 是同步更新,不用像异步持续独立的协程 SateFlow 那样收集起来有生命周期问题。

生命周期感知Android 处于后台时会跳过重组,页面不可见绘制不会发生,状态会正常记录并在界面恢复显示时重组刷新。
主线程保证内部确保更新在主线程分发(在协程中更新不用做额外处理)。
  • 状态若是一个可变对象(例如MutableList),更改 MutableList 中的 element 不会触发重组,需要的是对象(MutableList)的改变,这是对象内部数据(element)的改变。
  • MutableState 在普通使用下更简洁但仅限Compose,StateFlow 在并发修改时 update() 方法原子更新更简洁。
引用数据类型fun <T> mutableStateOf(
value: T, //默认值
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T>
ListmutableStateListOf( )
MapmutableStateMapOf( )
Number

mutableIntStateOf( )
mutableLongStateOf( )
mutableDoubleStateOf( )
mutableFloatStateOf( )

避免开装箱产生的性能消耗。

ViewModel { //方式一:使用赋值运算符获取的是MutableState对象,需要.value获取值再进行操作 private val _strState = mutableStateOf<String>("Hello Word!") val strState = _strState as State<String> //方式二:使用属性委托直接通过变量名对value进行读写 //属性委托后 getter setter 对应的是容器中的值而不再是容器,直接收窄 getter 对外就是只读了 //由于得到的不再是容器,后期拿去赋值传递就失去了可观察能力,只能直接用在界面使用 var strState2 by mutableStateOf("") private set } @Composable fun Input( viewModel:MainViewModel = viewModel() ) { Text(text = viewModel.strState.value) //方式一 Text(text = viewModel.strState2) //方式二 }

三、状态持久化

持久化存储时别把对象本身一股脑塞进去,应该是减少保存内容:能保存 ID 就保存 ID,能保存页码就保存页码、能保存输入字符串就保存字符串,大对象交给 ViewModel 或数据层恢复。

重组配置更改进程被杀后重建是否需要序列化
remember()✅❌❌不需要,可存任意对象
retain()✅✅❌不需要,可存任意对象
rememberSaveable()✅✅✅需要,Bundle支持类型

remember()

只跨重组

只是为了避免重组时重复创建对象。比如内部动画状态、简单展开状态、根据参数创建的 UI 辅助对象,或者某个不想在每次重组时重新构造的小状态持有者。

retain()

自定义类型

对象不能序列化,或者保存和恢复成本太高,同时又只需要在进程还活着的短暂重建场景里保留。

rememberSaveable()

用户可见状态

界面重建后不能丢失的状态如:用户输入、滚动位置、开关状态、筛选条件、当前展开项。

3.1 remember()

remreber( )可以为可组合函数提供数据持久化的功能,remember() 会记住所修饰的值(存储在UI树中,随可组合项移除而销毁),这样在重组后状态不会被初始化(在有添加或移除元素、重组发生在父元素中、配置更改后Activity重建、进入回退栈、进程被系统杀死后重建的情况下,使用 remember 会造成数据丢失变成初始值,见下方 rememberSaveable() 存储方式)。

在组合函数中创建的 MutableState 需要对其进行 remember 操作记住该值,否则每次重组都会初始化成默认值丢失数据,只有在调用方不需要管理状态的时候使用。由于内部创建了状态,难以复用和测试,见下方状态提升。

remember()

@Composable

inline fun <T> remember(

crossinline calculation: @DisallowComposableCalls () -> T

): T

记住Lambda中生成的值。

@Composable

inline fun <T> remember(

key: Any?,

crossinline calculation: @DisallowComposableCalls () -> T

): T

当key值变化时,就会执行Lambda重新生成值。所有影响 Lambda 中值生成的依赖都要包含进key(比如 itemId、user、config 等),确保依赖变化时旧对象能被替换。

//记录点击多少次 @Composable fun Demo() { //方式一:使用赋值运算符拿到的是remember()所修饰的MutableState对象。 val count = remember { mutableStateOf(0) } Button(onClick = { count.value++ }) { Text(text = "点击了 ${count.value} 次") } //方式二:属性委托直接操作变量名来读写value。 //需要导包androidx.compose.runtime.getValue和setValue。 var count by remember { mutableStateOf(0) } //记录点击多少次 Button(onClick = { count++ }) { Text(text = "点击了 $count 次") } //方式三:解构的是某个实现类,value是显示的值,setValue是如何处理新值(事件的处理)。 val (value, setValue) = remember { mutableStateOf("默认值") } TextField(value = value, onValueChange = setValue) //通过key控制 val num by remember { mutableStateOf(0) } val str = remember(key1 = num) { "数字是$num" } }
@Composable fun Demo(str: String) { //传入的参数变化了就重新计算 //否则避免每次都计算浪费性能 val length = remember(str) { str.length } Text("字符串长度是:$length") }

3.2 rememberSaveable()

与 remember 类似,用于 Activity 或进程重建时恢复界面状态,类似于 onSaveInstanceState( ),任何可以存储在 Bundle 中的数据都可以通过 rememberSaveable( ) 进行存储。适用于长期状态(如输入框、分页位置)。

rememberSaveable()

@Composable
public fun <T : Any> rememberSaveable(

vararg inputs: Any?,

init: () -> T

): T

@Composable
public fun <T> rememberSaveable(
vararg inputs: Any?,
stateSaver: Saver<T, out Any>,
init: () -> MutableState<T>,
): MutableState<T>

3.2.1 @Parcelize

直接对数据类使用该注解,类似于 Java 中的 Serializable 都是将实例编码成字节流存储,但不会产生大量临时对象、没有反射效率更高,也不用写 Parcelable 模板代码。如果不涉及到本地化存储或者网络传输推荐使用。

// 第一步:添加插件 plugins { id 'kotlin-parcelize' } //第二步:添加注解及Parcelable @Parcelize data class City(val name: String, val country: String) : Parcelable //第三步:保存到状态中 val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}

3.2.2 MapSaver()

不适合用Parcelize的场景可以使用,定义自己的存储和回复规则,规定如何把实例转为可保存到 Bundle 中的值。通过 save 这个 lambda 可以将 Book 对象转化为一个 Map 进行存储;要使用的时候就通过 restore 这个 lambda 将 Map 又恢复为一个 Book 对象。

data class Book(val name: String, val author: String) val BookSaver = run { val nameKey = "Name" val authorKey = "Author" mapSaver( save = { mapOf(nameKey to it.name, authorKey to it.author) }, restore = { Book(it[nameKey] as String, it[authorKey] as String) } ) } val chosenBook = rememberSaveable( stateSaver = BookSaver ) { mutableStateOf(Book("三体","刘慈欣")) }

3.2.3 ListSaver()

List 相对于上面的 Map 不用定义 key。

val BookListSaver = listSaver<Book, Any>( save = { listOf(it.name, it.author) }, restore = { Book(it[0] as String, it[1] as String) } ) val chosenBook = rememberSaveable( stateSaver = BookSaver ) { mutableStateOf(Book("三体","刘慈欣")) }

3.3 retain()

任意数据类型都能保存,由于是保存在内存里,需要注意释放问题(如key的设置)。只要进程还活着,它就有机会把对象保留下来;进程一死,这些内存里的对象也就没了。它能覆盖一些“临时销毁再回来”的场景,比如导航目的地暂时进入 back stack、不再处于当前 composition,或者 Activity 因配置变更销毁重建。但它不能覆盖系统杀进程后的恢复。

它的价值不是“活得更久”,而是“不用保存成另一种格式,也能拿回同一个对象”。只有保存不了、没必要跨进程恢复,或者保留同一个实例更重要时,retain 才是更合适的工具。

retain()

@Composable
public inline fun <reified T> retain(

noinline calculation: () -> T

): T

@Composable
public inline fun <reified T> retain(

vararg keys: Any?,

noinline calculation: () -> T

): T

四、状态提升 State Hoisting

根据组合项中是否有 State 可分为有状态组合项(Stateful)和无状态组合项(Stateless)。有状态组合项复用性不高难做测试 。

有状态组合变为无状态组合需要状态提升,即将状态(数据源和事件) 移出到组合外给调用方处理,具体是将 State 替换为两个形参:要显示的数据(value),对事件的操作 (T) -> Unit,T是新值。状态提升事件下降即单向数据流设计。

  • 状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项。
  • 状态应至少提升到它可以发生变化(写入)的最高级别。
  • 如果两种状态发生变化以响应相同的事件,它们应该一直提升。
//有状态 @Composable fun InputWithState() { val str = remember { mutableStateOf("Hello Word!") } Column { Text(text = str.value) TextField(value = str.value, onValueChange = { str.value = it }) } } //状态提升(中间层或底层不必传入ViewModel) @Composable fun InputWithoutState(value: String, onValueChange: (String) -> Unit) { Column { Text(text = str) TextField(value = value, onValueChange = onValueChange) } } //ViewModel中处理 var strState = mutableStateOf("Hello Word!") fun onStrChange(newValue: String) { strState.value = newValue } //Activity中各部分全部组合起来(只在顶层组合传入ViewModel) @Composable fun MainActivityScreen(viewMidel:MyViewModel) { InputWithoutState(viewMidel.strState, viewMidel.onStrChange) }

六、重组性能优化

6.1 使用父组合项中高频变化的状态时采用Lambda传值

由于 Column、Row、Box等是内联函数,编译后不是一个函数,如果内部有读取状态的行为,实际是外层在读取和重组,因此会引发不必要的外层重组。不要在父组合项中读取状态后再传递值给子组合项,而是子组合项的参数类型使用 Lambda,让子组合项读取。简而言之就是尽可能将读取状态的行为延后。

在Compose自带的关于偏移、可见度、大小变化的 API 中都有一个 Lambda 版本,效率更高因为可以跳过重组的过程只重新绘制或布局。

@Preview(showBackground = true) @Composable fun DemoPre() { Column(modifier = Modifier.background(Color.White)) { Demo1() Demo2() } } class DemoViewModel : ViewModel() { var aState by mutableStateOf("A:默认") //A使用属性委托 val bState = mutableStateOf("B:默认") //B使用赋值运算符 var c1State by mutableStateOf("C1:默认") //C1使用属性委托 val c2State = mutableStateOf("C2:默认") //C2使用赋值运算符 fun updateA() { aState = "A:已更新" } fun updateB() { bState.value = "B:已更新" } fun updateC1() { c1State = "C1:已更新" } fun updateC2() { c2State.value = "C2:已更新" } } //使用外部传入的状态 @Composable fun Demo1( viewModel: DemoViewModel = viewModel() ) { SideEffect { Log.e("发生重组", "Demo") } Column {//内联函数,内部读取状态的话,实际是Demo在读取状态,状态变化Demo就会重组 //aState是属性委托方式,调用变量名直接就是读取状态,因此会引发Demo重组 TestA(str = viewModel.aState) //bState是赋值运算符方式,调用变量名获取的是MutableState对象,不会引发Demo重组 TestB(str = viewModel.bState) //通过Lambda传参,两种方式都不会引起Demo重组 TestC { viewModel.c1State } //属性委托 TestC { viewModel.c2State.value } //赋值运算符 //值写死,由于没有读取状态,不会有任何重组 TestD(str = "D的值写死") Button(onClick = {viewModel.updateA()}) { Text(text = "更新A状态") } Button(onClick = {viewModel.updateB()}) { Text(text = "更新B状态") } Button(onClick = {viewModel.updateC1()}) { Text(text = "更新C状态-委托模式") } Button(onClick = {viewModel.updateC2()}) { Text(text = "更新C状态-赋值运算符") } } } //使用内部持有的状态(注释同上,效果一样) @Composable fun Demo2() { var aState by remember { mutableStateOf("A:默认") } val bState = remember { mutableStateOf("B:默认") } var c1State by remember { mutableStateOf("C1:默认") } val c2State = remember { mutableStateOf("C2:默认") } SideEffect { Log.e("发生重组", "Demo") } Column { TestA(str = aState) TestB(str = bState) TestC {c1State} TestC {c2State.value} TestD(str = "D的值写死") Button(onClick = { aState = "A:已更新" }) { Text(text = "更新A状态") } Button(onClick = { bState.value = "B:已更新" }) { Text(text = "更新B状态") } Button(onClick = { c1State = "C1:已更新" }) { Text(text = "更新C状态-委托模式") } Button(onClick = { c2State.value = "C2:已更新" }) { Text(text = "更新C状态-赋值运算符") } } } @Composable fun TestA(str: String) { SideEffect { Log.e("发生重组", "A") } Text(text = str) } @Composable fun TestB(str: MutableState<String>) { SideEffect { Log.e("发生重组", "B") } Text(text = str.value) //这里调用.value才是读取状态 } @Composable fun TestC(str:() -> String) { SideEffect { Log.e("发生重组", "C") } Text(text = str()) //这里才是Lambda实际调用的地方(状态读取) } @Composable fun TestD(str: String) { SideEffect { Log.e("发生重组", "D") } Text(text = str) }

6.2 使用派生状态来降低重组次数 derivedStateOf( )

一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。可对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用 remember 的 key 可以实现,有些情况的状态无法用作key,例如元素改变了而 List 没变。

derivedStateOf()public fun <T> derivedStateOf(calculation: () -> T): State<T>

举例一:有一个变化频率非常高的数值,我们只关心正负,正为绿色负为红色。优化前每次数值变化都会导致重组(例如从0.2变成0.3颜色是不变的却进行了重组),优化后只根据正负变化重组。

@Composable fun Demo() { var value by remember{ mutableStateOf(0F) } //监听value的变化派生出isPositive val isPositive by remember { derivedStateOf { value >= 0 } } SideEffect { Log.e("发生重组", "Demo") } Column { Row { Button(onClick = { value += 0.1F }) { Text(text = "点击+0.1") } Button(onClick = { value -= 0.1F }) { Text(text = "点击-0.1") } } //这里如果是以 value>=0 为条件,每次数值变化都会引起Demo重组 //将条件换成 isPositive 只有正负变化才会引起Demo重组 Box(modifier = Modifier .size(50.dp) .background(if(isPositive) Color.Green else Color.Red) //正数显示绿色,负数显示红色 ){ SideEffect { Log.e("发生重组", "Box") } } } }

七、状态转换

7.1 LiveData、Flow、RxJava → State。

在 Compose 中读取其它可观测类型之前,必须将其转换为 State<T>,以便可组合项可以在状态发生变化后自动重组。

这些方法底层已做 remember 持久化处理。

LiveData

@Composable
public fun <T> LiveData<T>.observeAsState(): State<T?>

@Composable
public fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R>
Flow

@Composable
public fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext,
): State<R>

@Composable
public fun <T> Flow<T>.collectAsStateWithLifecycle(
initialValue: T,
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext,
): State<T>

//仅适用于Android,处于后台不收集

//需要导包 'androidx.lifecycle:lifecycle-runtime-compose:2.6.1'

implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.runtime:runtime-rxjava2:$compose_version" implementation "androidx.compose.runtime:runtime-flow:$compose_version"
@Composable fun Demo( viewModel:DemoViewModel = viewModel() ) { //LiveData val dataState by viewModel.data.observeAsState() Text("Username: ${dataState}") //Flow val uiState by viewModel.user.collectAsState(ViewState.Loading) //RxJava val uiState by viewModel.user.subscribeAsState(ViewState.Loading) when (uiState) { ViewState.Loading -> TODO("Show loading") ViewState.Error -> TODO("Show Snackbar") is ViewState.Content -> TODO("Show content") } } class DemoViewModel : ViewModel() { //LiveData private val _data = MutableLiveData("") val data = _data as LiveData<String> //Flow // val user: Flow<ViewState> = flowOf(ViewState.Loading) //冷流无意义(后期无法发送值) val user: MutableStateFlow<ViewState> = MutableStateFlow(ViewState.Loading) //RxJava // val user: Observable<ViewState> = Observable.just(ViewState.Loading) }
// flow不能封装成一个用于设置最大计数和间隔时间的函数中,会不停重组维持最大数 val countDownTimer by flow { repeat(5) { emit(it) delay(1000) } }.collectAsState(initial = 5) @Composable fun Test() { //显示5 4 3 2 1 Text("倒计时:${countDownTimer}") }

7.2 协程 → State(produceState)、State → Flow(snapshotFlow)

详见附带效应

八、状态持有者

考虑将状态保存在何处的因素有:界面状态(数据还是UI)、逻辑(业务还是UI)

组合项如果状态数量较少和逻辑比较简单,在组合项中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个组合项进行。但是如果将它传递给其它组合项,就不符合单一可信来源,而且会使调试更困难。
状态容器当组合项涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了组合的复杂性。保证组合项只是负责展示,而状态容器负责逻辑和状态。
ViewModelViewModel 的生命周期往往是比较长的,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航(如果使用了导航库)的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和组合密切相关的一些状态,否则,可以会导致内存泄漏。如果 ViewModel 中包含要在进程重建后保留的状态,使用SavedStateHandle。

8.1 组合 作为可信来源

val scaffoldState = rememberScaffoldState() val coroutineScope = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { MyContent( showSnackbar = { message -> coroutineScope.launch { scaffoldState.snackbarHostState.showSnackbar(message) } } ) }

8.2 状态容器 作为可信来源

class MyAppState( val scaffoldState: ScaffoldState, val navController: NavHostController, private val resources: Resources ) { val bottomBarTabs = /* State */ val shouldShowBottomBar: Boolean get() { //何时显示底栏的逻辑 } fun navigateToBottomBarRoute(route: String) { //导航逻辑 } fun showSnackbar(message: String) { //使用资源显示 snackbar } } //使用MyAppState 的时候需要使用remember来进行信赖 //可以创建一个rememberMyAppState方法来直接返回MyAppState实例。 @Composable fun rememberMyAppState( scaffoldState: ScaffoldState = rememberScaffoldState(), navController: NavHostController = rememberNavController(), resources: Resources = LocalContext.current.resources, ) = remember(scaffoldState, navController, resources, /* ... */) { MyAppState(scaffoldState, navController, resources, /* ... */) } //使用 val myAppState = rememberMyAppState() Scaffold( scaffoldState = myAppState.scaffoldState, bottomBar = { if (myAppState.shouldShowBottomBar) { BottomBar( tabs = myAppState.bottomBarTabs, navigateToRoute = { myAppState.navigateToBottomBarRoute(it) } ) } } ) { NavHost(navController = myAppState.navController, "initial") { /* ... */ } }

8.3 ViewModel 作为可信来源

在函数式编程一大特点就是「不可变性」,即在函数式编程中,状态不是通过修改变量,而是通过创建和返回新的状态来更新的。这意味着数据是不可变的(immutable),一旦创建,就不能更改。通过这种不可变性尽可能的避免副作用。

class LoginVM : ViewModel() { var uiState by mutableStateOf(LoginUiState()) private set private fun toLogin() { uiState = uiState.copy(isFirstRefresh = false) //修改状态 } } data class LoginUiState( val datas: List<VisitorBean> = emptyList(), //为状态提供默认值 val isFirstRefresh: Boolean = true )
@Composable fun LoginScreen( viewModel: LoginVM = viewModel() ) { if(viewModel.isFirstRefresh) {...} //读取状态 }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询