一、什么是状态
大到一个页面的切换,小到一个字符的增删,这些看得见的变化,本质上都是内部数据的变化,这些不断变化的数据就是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从入门到实战
其他参考内容:
初学者如有错误欢迎批评指正!