一、什么是状态
大到一个页面的切换,小到一个字符的增删,这些看得见的变化,本质上都是内部数据的变化,这些不断变化的数据就是UI的"状态"。
在传统视图体系中,状态大多以View的成员变量形式存在,例如当想要更新TextView的文字时,通常先要设法获取TextView的实例,然后调用setText方法对文本内容进行更新。随着各处关于TextView的需求增多,相似的代码也会增多,我们也需要一遍遍的编写相似的代码,且随着业务逻辑变复杂setText的逻辑也会变得很复杂。在这里TextView倾向于持有自己的状态,如果没有ViewModel、LiveData等框架的支持,很难写出符合单向数据流架构的代码。
Compose在设计之初就贯彻了单向数据流的设计思想:首先Composable只是一个函数,不会像View那样轻易封装私有状态,状态随处定义的情况得到抑制;其次Compose的状态像LiveData一样能够被观察,当状态变化后,相关联的UI会自动刷新,不需要像传统视图那样命令式地逐个通知。因此即使没有ViewModel和LiveData的加持,也能轻松写出符合单向数据流架构的代码。
接下来就看看Compose是如何实现这一切的,首先深入了解一下Compose的"状态"。
二、Stateless与Stateful
传统视图中通过获取组件对象句柄来更新组件状态,而Compose中Composable只是一个函数,且调用后不返回任何实例,那么Composable是如何实现UI刷新的呢?
重新执行@Composable函数来更新UI的过程被称为重组,所以Composable是通过重组来刷新UI的,而重组正是由于Composable的状态变化所触发的。
只依赖函数传参来构建不同UI界面的Composable被称为Stateless Composable(无状态的可组合项)。相对的,有的Composable内部持有或者访问了某些状态,我们称为Stateful Composable(有状态的可组合项)。Stateless Composable的重组只能来自上层Composable的调用,而Stateful Composable的重组来自其依赖状态的变化。
当Stateless的参数没有变化时不会参与调用方的重组,重组范围局限在Stateless外部,如图所示:

三、状态的定义
看下面的一个简单的例子:
kotlin
@Composable
fun Greeting() {
var countNumber by remember {
mutableStateOf(0) //1
}
Column {
Text(text = "当前计数: $countNumber") //重组刷新计数
Button(onClick = { countNumber++ }) { //增加以修改数值
Text(text = "增加计数")
}
}
}
UI效果

上述代码中依赖对countNumber的读写,因此它是一个Stateful Composable。countNumber是Int类型,那是如何修改countNumber来实现状态改变并发生重组的呢?我们先来看看注释1处的mutableStateOf(0),点进去跟踪源码如下:
kotlin
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() //快照机制,定义了如何比较特定类型的值 (equivalent) 以及如何解决冲突 (merge)
): MutableState<T> = createSnapshotMutableState(value, policy)
kotlin
@Stable
interface MutableState<T> : State<T> {
override var value: T //持有值
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
kotlin
@Stable
interface State<out T> {
val value: T //持有值
}
本来想分析一下源码,发现插入在这里太长了,初学阶段还是先讲究实用吧(^o^)/~。
总结就是快照系统会把所有订阅了State的RecomposeScope记录下来,当State的值value发生变化时,会通知所有的RecomposeScope进行重组。
创建MutableState的三种方式:
kotlin
//返回T类型的value
var countNumber = remember {
mutableStateOf(0)
}
//通过解构返回T类型的value以及(T)→Unit类型的set方法
var (countNumber,setCountNumber) = remember {
mutableStateOf(0)
}
//使用属性代理。
//对countNumber的读写会通过getValue和setValue这两个运算符的↩️
//重写最终代理为对value的操作,通过by关键字,可以像访问一个普通的Int变量一样对状态进行读写。
var countNumber by remember {
mutableStateOf(0)
}
//委托给了下面二个函数
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
当使用by代理创建State时,需要额外引入以下扩展方法:
kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
有时IDE无法自动import上面的依赖,如果发现编译报错,可以手动添加依赖。代理的方式在后续使用中最为简单,所以也是实际项目中的首选。
上述三种方式都使用到了remember,那remember的作用是什么? 看下源码:
kotlin
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation) //对值做缓存
kotlin
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
@Suppress("UNCHECKED_CAST")
return rememberedValue().let {
if (invalid || it === Composer.Empty) {
val value = block() //对mutableStateOf的调用获取新的值
updateRememberedValue(value) //记录新的值做缓存
value
} else it
} as T
}
mutableStateOf创建可变状态变量 。mutableStateOf 函数用于创建可变状态(mutable state)。它接受一个初始值作为参数,并返回一个包含此值的可变状态变量。可变状态变量的值可以更改,在 Compose 中需要在 Composable 函数内部使用。
remember记录可变状态变量并保证使用时的统一和重建时状态的恢复 。remember 函数用于将值保留在 Composable 函数内。它可以用来在 Composable 函数的多次调用之间保留不变的值,以避免因组件重组而导致的数据混乱和丢失。remember 函数接受一个 lambda 表达式作为参数,这个 lambda 表达式通常包含对 mutableStateOf 的调用,用于创建和保留一个可变状态的变量。
务必记住,mutabeStateOf的调用一定要出现在remember中,不然每次重组都会创建新的状态。
四、状态的持久化与恢复
前面说到,remember可以缓存创建的状态,避免因为重组而丢失。使用remember缓存的状态虽然可以跨越重组,但是不能跨越Activity或者跨越进程存在。比如当横竖屏等ConfigurationChanged事件发生时,状态会发生丢失。如果想要更长久地保存状态,就需要使用到rememberSavable了,它可以像Activity的onSaveInstanceState那样在进程被杀死时自动保存状态,同时像onRestoreInstanceState一样随进程重建而自动恢复。
rememberSavable中的数据会随onSaveInstanceState进行保存,并在进程或者Activity重建时根据key恢复到对应的Composable中,这个key就是Composable在编译期被确定的唯一标识。因此当用户手动退出应用时,rememberSavable中的数据才会被清空。
rememberSavable实现原理实际上就是将数据以Bundle的形式保存,所以凡是Bundle支持的基本数据类型都可以自动保存。对于一个对象类型,则可以通过添加@Parcelize变为一个Parcelable对象进行保存。比如下面的代码中City就是一个Parcelable类,而MutableState本身也是一个Parcelable对象,因此可以直接保存进rememberSavable。
有的数据结构可能无法添加Parcelable接口,比如定义在三方库的类等,此时可以通过自定义Saver为其实现保存和恢复的逻辑。只需要在调用rememberSavable时传入此Saver即可:
kotlin
object CitySaver : Saver<City, Bundle> {
override fun restore(value: Bundle): City? {
//恢复
return value.getStrinq("name")?.let { name ->
value.getString("country")?.let { country ->
City(name, country)
}
}
}
override fun SaverScope.save(value: City): Bundle? {
//存值
return Bundle().apply {
putString("name", value.name)
putString("country", value.country)
}
}
}
//使用
@Composable
fun CityScreen() {
var selectedCity = rememberSavable(stateSaver = CitySaver) { //传入Saver
mutableStateof(City("Madrid", "Spain"))
}
}
除了自定义Saver外,Compose也提供了MapSaver和ListSaver供开发者使用。
五、使用ViewModel管理状态
前面学习了rememberSavable,它可以在屏幕旋转时甚至进程被杀死时保存状态,理论上可以替代ViewModel的存在。但是一个真实的项目,业务逻辑不会只是对countNumber的加加减减这样简单,往往要复杂得多,如果这些代码都放在Stateful Composable中,会导致UI组件的职责不清,毕竟Composable的主要职责是负责UI的显示。所以当Stateful的业务逻辑变得越发复杂时,可以将Stateful的状态提到ViewModel管理,Stateful也就变为了一个Stateless,通过参数传入不同ViewModel即可替换具体业务逻辑,可复用性和可测试性也大大提高。
Compose中使用ViewModel需要导包,如下:
kotlin
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
下面是潦草的示例代码
kotlin
class MainViewModel : ViewModel() {
private val _countNumber = mutableStateOf(0)
//对外暴露不可改变的State
val countNumber: State<Int> = _countNumber
fun add() {
_countNumber.value = _countNumber.value + 1
}
}
//使用
@Composable
fun Greeting() {
val mainViewModel: MainViewModel = viewModel() //注意这里的viewModel()
CounterComponent(mainViewModel.countNumber,mainViewModel::add)
}
viewModel()是一个@Composable方法,用于在Composable中创建ViewModel。Greeting()函数中通过viewModel()创建了一个MainViewModel对象, MainViewModel持有可变状态值countNumber。看下viewModel()的源码如下:
kotlin
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(...略...): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory, extras)
有没有注意到ViewModel中的mutableStateOf并没有被remember{}包裹,这是为什么呢?
答:这是因为在Activity销毁之前,ViewModel将一直存在,viewModel()每次调用将返回同一个实例,所以此时可以不使用remember{}进行缓存。
六、LiveData、RxJava、Flow转State
在MVVM架构中,View通过LiveData等观察ViewModel的状态,当LiveData的数据变化时,会以数据流的形式通知View,因此LiveData这类工具也被称为流式数据框架或响应式数据框架。同类框架还有RxJava、Flow等。在Compose中同样的功能由State负责完成,可以将上述这些流式数据转换为Composable的State,当LiveData等数据变化时,可以驱动Composable完成重组。
| 扩展方法 | 依赖库 |
|---|---|
| LiveData. observeAsState | androidx.compose.runtime: runtime-livedata: $ composeVersion |
| Flow.collectAsState() | 不依赖三方库,Compose自带 |
| Observable.subscribeAsState() | androidx.compose.runtime: runtime-rxjava3: $ composeVersion |
如果你正打算往项目中引入响应式框架,从包体积以及Compose的兼容性角度考虑,Flow是首选方案,如果是一个Compose first项目,那么推荐在ViewModel中直接使用State。
七、状态的分层管理
1、使用Stateful管理状态
简单的UI状态以及配套逻辑适合在Composable中直接管理。
2、使用StateHolder管理状态
StateHolder并不是一个类或函数,而是指使用普通类或者数据类管理状态的一种方式,称为StateHolder(状态容器)。
状态会产生逻辑,随着UI状态的增多,UI逻辑也越发复杂,此时可以多个状态连同相关逻辑一起放进专门的StateHolder进行管理。剥离UI逻辑的Composable可以专注UI布局,符合关注点分离的设计原则。
kotlin
// Plain class that manages App's UI logic and UI elements' state
class MyAppState( //StateHolder状态容器
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources
) {
val bottomBarTabs = /* State */
// Logic to decide when to show the bottom bar
val shouldShowBottomBar: Boolean
get() = /* ... */
// Navigation logic, which is a type of UI logic
fun navigateToBottomBarRoute(route: String) { /* ... */ }
// Show snackbar using Resources
fun showSnackbar(message: String) { /* ... */ }
}
@Composable
fun rememberMyAppState( //定义配套的remember方法
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources
) = remember(scaffoldState, navController, resources) { //remember函数做可变状态值的缓存,下方展示源码
MyAppState(scaffoldState, navController, resources)
}
//上面remember(...)的源码为:
@Composable
inline fun <T> remember(
vararg keys: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
var invalid = false
for (key in keys) invalid = invalid or currentComposer.changed(key)
return currentComposer.cache(invalid, calculation) //缓存
}
随着MyApp的组件变多,Snackbar、Bottombar、Navigation等各种逻辑越来越复杂,相关逻辑代码已经不适合直接写在MyApp中了,此时使用MyAppState这个StateHolder对状态进行统一管理。
由于StateHolder要使用remember保存在Composable中,所以需要为StateHolder定义一个配套的remember方法,便于在Composable中创建和使用。
StateHolder将逻辑抽离后,MyApp只关注UI布局,使职责变得更加清晰:
kotlin
@Composable
fun MyApp() {
MyTheme {
val myAppState = rememberMyAppState() //配套的remember方法
Scaffold(
scaffoldState = myAppState.scaffoldState, //使用
bottomBar = {
if (myAppState.shouldShowBottomBar) { //使用
BottomBar(
tabs = myAppState.bottomBarTabs, //使用
navigateToRoute = {
myAppState.navigateToBottomBarRoute(it) //使用
}
)
}
}
) {
NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}
}
}
StateHolder无法像ViewModel那样在横竖屏切换等ConfigurationChanged发生时自动恢复,但是可以通过rememberSavable帮它实现同样的效果。
3、使用ViewModel管理状态
从某种意义上讲,ViewModel只是一种特殊的StateHolder,但因为它保存在ViewModelStore中,所以有以下特点:
- 存活范围大:可以脱离
Composition存在,被所有Composable共享访问。 - 存活时间长:不会因为横竖屏后者进程被杀死等情况丢失状态。
因此ViewModel适合管理应用级别的全局状态,各Composable可以通过viewModel()获取ViewModel单例达到"全局共享"的效果,而且ViewModel更倾向于管理那些非UI的业务状态 ,因为业务状态中的数据往往需要脱离UI长期保存,否则有可能造成内存泄漏。
kotlin
data class ExampleUiState(
dataToDisplayOnScreen: List<Example> = emptyList(),
userMessages: List<Message> = emptyList(),
loading: Boolean = false
)
class ExampleViewModel( //ViewModel
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf<ExampleUiState>(...)
private set //私有化set函数,防止外界修改uiState
// Business logic
fun somethingRelatedToBusinessLogic() { ... }
}
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) { //ViewModel作为参数
val uiState = viewModel.uiState //使用ViewModel中的uiState
...
Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
Text("Do something")
}
}
在上面的代码中,ExampleUiState中包含了userMessages这样的领域层数据,以及loading这样的代表数据加载状态的数据,这些都与UI无关,适合用ViewModel进行管理。此外,如果 ViewModel 中包含要在进程重建后保留的状态,请使用SavedStateHandle,ViewModel通过SavedStateHandler实现uiState的持久化保存。
先导包
kotlin
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2"
下面是一个例子
kotlin
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
companion object {
private const val KEY_COUNT = "count"
}
fun getCount(): Int {
return savedStateHandle.get(KEY_COUNT) ?: 0
}
fun incrementCount() {
val count = getCount()
savedStateHandle.set(KEY_COUNT, count + 1)
}
}
//使用
val mainViewModel:MainViewModel = viewModel()
val count = mainViewModel.getCount()
ViewModel的另一个优势是支持Hilt依赖注入,尤其是当业务逻辑依赖Repository等数据层对象时,通过配合hilt-navigation-compose组件库的使用,可以为每个页面的ViewModel实例自动注入所需的依赖。
ViewModel与StateHolder也可以同时使用,两者各司其职。StateHolder可以用来管理UI相关的状态和逻辑,ViewModel可以用来管理与UI无关的状态和逻辑。下面的代码是一个StateHolder与ViewModel并存的例子:
kotlin
private class ExampleState( //UI状态
val lazyListState: LazylistState,
private valresources: Resources,
private val expandedItems: List<Item> = emptyList()
) { ... }
@Composable
private fun rememberExampleState(...) {...} //配套的remember方法
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState //ViewModel
val exampleState = rememberExampleState() //StateHolder
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item) {
...
}
...
}
}
}
4、状态分层管理的最佳实践
因为ViewModel在ViewModelStoreOwner的范围内只有唯一实例,所以更适合存储全局唯一状态,当State需要多实例 存在时,建议使用StateHolder进行管理。总的来说,在Compose中应该根据状态和逻辑的复杂度以及业务类型,选择不同的状态管理方式。在一些复杂场景中,多种管理方式也可能并存,如图所示:

Composable主攻UI的布局,可以持有少量UI状态。当UI逻辑较多时,可以依赖StateHolder管理,Composable同时依赖多个StateHolder负责不同的UI逻辑,StateHolder与Composable都保存在Composable视图树上,所以所辖状态的生命周期与所处的Composable一致。
Composable或者StateHolder可以依赖ViewModel管理UI无关的状态及对应的业务逻辑。借助ViewModel,这些状态可以跨越Composable甚至Activity的生命周期长期存在。ViewModel依赖处于更底层的领域层或者数据层完成相关业务,从上图中可知道,处于底层的业务服务范围往往更广,存活时间也更长。
参考了以下内容:
本文大部分内容参考了实体书 Jetpack Compose从入门到实战
其他参考内容:
初学者如有错误欢迎批评指正!