参考文章1参考文章2官方页面一、概念Compose将界面可组合函数和数据状态分开事件会引起状态的变化进而使读取了该状态的组件会发生重组。MutableState 会与读取它的组合函数绑定当所持有的状态值发生变化时通知组件进行界面刷新重组。remember()、rememberSaveable()、retain() 提供持久化存储功能因为使用了纯函数形式表达界面如果在函数中声明属性局部变量会因为函数被多次调用而丢失状态值。二、MutableStateCompose仅对“可观察”的状态变化做出响应普通变量变化无法感知不会触发重组。MutableState 是 Compose 专用的观察者模式的容器同LiveData、StateFlow一样可以观察到值的变化会与订阅它的组件绑定当所持有的值发生变化时通知组件进行重组刷新。MutableState 是同步更新不用像异步持续独立的协程 SateFlow 那样收集起来有生命周期问题。生命周期感知Android 处于后台时会跳过重组页面不可见绘制不会发生状态会正常记录并在界面恢复显示时重组刷新。主线程保证内部确保更新在主线程分发在协程中更新不用做额外处理。状态若是一个可变对象例如MutableList更改 MutableList 中的 element 不会触发重组需要的是对象MutableList的改变这是对象内部数据element的改变。MutableState 在普通使用下更简洁但仅限ComposeStateFlow 在并发修改时 update() 方法原子更新更简洁。引用数据类型fun T mutableStateOf(value: T, //默认值policy: SnapshotMutationPolicyT structuralEqualityPolicy()): MutableStateTListmutableStateListOf( )MapmutableStateMapOf( )NumbermutableIntStateOf( )mutableLongStateOf( )mutableDoubleStateOf( )mutableFloatStateOf( )避免开装箱产生的性能消耗。ViewModel { //方式一使用赋值运算符获取的是MutableState对象需要.value获取值再进行操作 private val _strState mutableStateOfString(Hello Word!) val strState _strState as StateString //方式二使用属性委托直接通过变量名对value进行读写 //属性委托后 getter setter 对应的是容器中的值而不再是容器直接收窄 getter 对外就是只读了 //由于得到的不再是容器后期拿去赋值传递就失去了可观察能力只能直接用在界面使用 var strState2 by mutableStateOf() private set } Composable fun Input( viewModelMainViewModel 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()Composableinline fun T remember(crossinline calculation: DisallowComposableCalls () - T): T记住Lambda中生成的值。Composableinline 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()Composablepublic fun T : Any rememberSaveable(vararg inputs: Any?,init: () - T): TComposablepublic fun T rememberSaveable(vararg inputs: Any?,stateSaver: SaverT, out Any,init: () - MutableStateT,): MutableStateT3.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 listSaverBook, 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()Composablepublic inline fun reified T retain(noinline calculation: () - T): TComposablepublic inline fun reified T retain(vararg keys: Any?,noinline calculation: () - T): T四、状态提升 State Hoisting根据组合项中是否有 State 可分为有状态组合项Stateful和无状态组合项Stateless。有状态组合项复用性不高难做测试 。有状态组合变为无状态组合需要状态提升即将状态数据源和事件 移出到组合外给调用方处理具体是将 State 替换为两个形参要显示的数据value对事件的操作 (T) - UnitT是新值。状态提升事件下降即单向数据流设计。状态应至少提升到使用该状态(读取)的所有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(viewMidelMyViewModel) { 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: MutableStateString) { 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): StateT举例一有一个变化频率非常高的数值我们只关心正负正为绿色负为红色。优化前每次数值变化都会导致重组例如从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) } } //这里如果是以 value0 为条件每次数值变化都会引起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 中读取其它可观测类型之前必须将其转换为 StateT以便可组合项可以在状态发生变化后自动重组。这些方法底层已做 remember 持久化处理。LiveDataComposablepublic fun T LiveDataT.observeAsState(): StateT?Composablepublic fun R, T : R LiveDataT.observeAsState(initial: R): StateRFlowComposablepublic fun T : R, R FlowT.collectAsState(initial: R,context: CoroutineContext EmptyCoroutineContext,): StateRComposablepublic fun T FlowT.collectAsStateWithLifecycle(initialValue: T,lifecycle: Lifecycle,minActiveState: Lifecycle.State Lifecycle.State.STARTED,context: CoroutineContext EmptyCoroutineContext,): StateT//仅适用于Android处于后台不收集//需要导包 androidx.lifecycle:lifecycle-runtime-compose:2.6.1implementation androidx.compose.runtime:runtime-livedata:$compose_version implementation androidx.compose.runtime:runtime-rxjava2:$compose_version implementation androidx.compose.runtime:runtime-flow:$compose_versionComposable 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 LiveDataString //Flow // val user: FlowViewState flowOf(ViewState.Loading) //冷流无意义后期无法发送值 val user: MutableStateFlowViewState MutableStateFlow(ViewState.Loading) //RxJava // val user: ObservableViewState 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 协程 → StateproduceState、State → FlowsnapshotFlow详见附带效应八、状态持有者考虑将状态保存在何处的因素有界面状态数据还是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: ListVisitorBean emptyList(), //为状态提供默认值 val isFirstRefresh: Boolean true )Composable fun LoginScreen( viewModel: LoginVM viewModel() ) { if(viewModel.isFirstRefresh) {...} //读取状态 }