Compose采取了声明式UI的开发范式。在这种范式中,UI的职责仅作为数据状态的反应。如果数据状态没有变化,则UI永远不会自行改变。如果把Composable的执行看作是一个函数运算,那么状态就是函数的参数,生成的布局就是函数的输出。
Stateless和Stateful
传统视图中通过获取组件对象句柄来更新组件状态,而Compose则通过重新执行Composable函数来更新页面(重组)。StatelessComposable只依赖参数的Composable;相对的,有些Composable内部持有或者访问了某些状态,称之为StatefulComposable。StatelessComposable的重组只能来自上层Composable的调用,而StatefulComposable的重组来自其以来的状态的变化。
kotlin
//Statelesscomposable
@Composable
fun Hello(name:String){
Text(text = "Hello $name")
}
//StatefulComposable
@Preview
@Composable
fun CounterComponent() {
Column(
modifier = Modifier.padding(16.dp)
) {
//remember中计算得到的数据会自动缓存,当Composable重组再次执行到remember处会返回之前已缓存的数据,无须重新计算。mutableStateOf的调用一定要出现在remember中,不然每次重组都会创建新的状态。
var counter by remember { mutableStateOf(0) }
Text( //1
"Click the buttons to adjust your value:",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text( //2
"$counter",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = typography.h3
)
Row {
Button(
onClick = { counter-- },
Modifier.weight(1f)
) {
Text("-")
}
Spacer(Modifier.width(16.dp))
Button(
onClick = { counter++ },
Modifier.weight(1f)
) {
Text("+")
}
}
}
}
在Compose中使用State<T>描述一个状态,泛型T是状态的具体类型。
kotlin
interface State<out T> {
val value: T
}
State<T>是一个可观察对象。当Composabel对State的value进行读取时会与State建立订阅关系,当value发生变化时,作为监听者的Composable会自动重组刷新UI。
有时候Composable需要对State的value进行修改,比如在CounterComponent中单击按钮需要修改counter的值,所以counter可被修改,使用MutableState<T>来表示可修改状态,其包裹的数据是一个可修改的var类型。
kotlin
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
MutableState有三种用法:
-
创建MutableState
kotlinvar counter:MutableState<Int> = mutableStateOf(0)
-
解构方式
kotlinval(counter,setCounter) = mutableStateOf(0)
此时的counter已经是一个Int类型的数据,后续使用的地方可以直接访问,无须再使用点操作符获取value,而需要更新counter的地方可以使用setCounter(xx)完成。
-
属性代理
使用by关键字直接获取Int类型counter。
kotlinvar counter by mutableStateOf(0)
by关键字的原理是对counter的读写会通过getValue和setValue这两个运算符的重写最终代理为对value的操作。
状态上提
状态上提的通常做法是将内部状态移除,通过参数传入需要在UI显示的状态,以及需要回调给调用方的事件,案例如下所示:
kotlin
@Composable
fun CounterComponent(
counter:Int,//重组时调用方传入当前需要显示的计数
onIncrement:()->Unit,//向调用方回调单击加号的事件
onDecrement:()->Unit,//向调用方回调单击减号的事件
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text( //1
"Click the buttons to adjust your value:",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text( //2
"$counter",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = typography.h3
)
Row {
Button(
onClick = { onDecrement() },
Modifier.weight(1f)
) {
Text("-")
}
Spacer(Modifier.width(16.dp))
Button(
onClick = { onIncrement() },
Modifier.weight(1f)
) {
Text("+")
}
}
}
}
状态的持久化与恢复
前面说到,remember可以缓存创建的状态,避免因为重组而丢失。使用remember缓存的状态虽然可以跨越重组,但是不能跨越Activity或跨越进程存在。如果想要更长久地保存状态,就需要使用到rememberSaveable了,它可以像Activity的onSaveInstanceState那样在进程被杀死时自动保存状态,同时onRestoreInstanceState一样随进程重建而自动恢复。
rememberSaveable中的数据会随onSaveInstanceState进行保存,并在进程或Activity重建时根据key恢复到对应的Composable中,这个key就是Composable在编译期被确定的唯一标识。因此当用户手动退出应用时,rememberSavable中的数据才会被清空。
rememberSaveable实现原理实际上就是将数据以Bundle的形式保存,所以凡是Bundle支持的基本数据类型都可以自动保存。对于一个对象类型,则可以通过添加@Parcelize变为一个Parcelable对象进行保存。当我们遇到有的数据结构可能无法添加Parcelable接口,此时可以通过自定义Saver为其实现保存和恢复的逻辑。只需要在调用rememberSaveable时传入此Saver即可:
kotlin
@Parcelize
data class City(val name: String,val country:String):Parcelable
object CitySaver:Saver<City,Bundle>{
override fun restore(value: Bundle): City? {
return value.getString("name")?.let {name ->
value.getString("country")?.let { country->
City(name = name,country=country)
}
}
}
override fun SaverScope.save(value: City): Bundle? {
return Bundle().apply {
putString("name",value.name)
putString("country",value.country)
}
}
}
@Preview
@Composable
fun WelcomePageLightPreview() {
WelcomePage()
val city = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("City",""))
}
}
除了自定义Saver外,Compose也提供了MapSaver和ListSaver供开发者使用,MapSaver将对像转换为Map <String,Any>的结构进行保存,注意value是可保存到Bundle的类型,同理,ListSaver则是将对像转换为List<Any>的数据结构进行保存。
kotlin
val cityMapSaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name,countryKey to it.country) },
restore = {City(it[nameKey] as String,it[countryKey] as String)}
)
}
val cityListSaver = run {
val nameKey = "Name"
val countryKey = "Country"
listSaver(
save = { listOf(it.name,it.country) },
restore = {City(nameKey,countryKey)}
)
}
使用ViewModel管理状态
各位读者可以参考下面这两篇博客:
https://juejin.cn/post/7105236042864656391
https://blog.csdn.net/u010976213/article/details/117284079
重组与自动刷新
智能的重组
Compose的重组非常"智能",当重组发生时,只有状态发生更新的Composable才会参与重组,没有变化的Composable会跳过本次重组。
避免重组的"陷阱"
由于Composable在编译期代码会发生变化,代码的实际运行情况可能不如预期的那样。所以需要了解composable在重组执行时的一些特性,避免陷入重组的"陷阱"。
-
Composable会以任意顺序执行
多个composable函数未必按照先后顺序执行,因此不能在composable设置一个全局变量,以期望在第一个composable中修改该值,在第二个composable修改为另一个值。
-
Composable会并发执行
重组中的Composable并不一定执行在UI线程,它们可能在后台线程池中并行执行,这有利于发挥多核处理器的性能优势。
-
Composable会反复执行
除了重组会造成Composable的再次执行外,在动画等场景中每一帧的变化都可能引起Composable的执行,因此Composable有可能会短时间内反复执行,我们无法准确判断它的执行次数。
-
Composable的执行是"乐观"的
所谓"乐观"是指composable最终总会依据最新的状态正确地完成重组。在某些场景下,状态可能会连续变化,这可能会导致中间态的重组在执行中被打断,新的重组会插入进来。对于被打断的重组,composable不会将执行一般的重组结果反应到视图树上,因为他知道最后一次状态总归是正确的,因此中间态丢弃也没关系。
总结:composable框架要求composable作为一个无副作用的纯函数运行,只要在开发中遵循这一原则,上述这一系列特性就不会成为程序执行的"陷阱",反而有助于提高程序的执行性能。
如何确定重组范围
经过composable编译器处理后的Composable代码在对state进行读取的同时,能够自动建立关联,在运行过程中当state变化时,Compose会找到关联的代码块标记为Invalid。在下一渲染帧到来之前,Compose会触发重组并执行invalid代码块,Invalid代码块即下一次重组的范围。能够被标记为Invalid的代码必须是非inline且无返回值的composable函数或lambda。因为inline函数在编译期间会在调用处展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。而对于有返回值的函数,由于返回值的变化会影响调用方,所以必须连同调用方一同参与重组,因此它不能单独作为Invalid代码块。
优化重组的性能
在编译期间,compose编译器会根据代码调用位置,为composable生成索引key,并存入composition。composable在执行过程中通过与key的对比,可以知道当前应该执行何种操作(增、删、更新、移动等多种变化)。
生命周期与副作用
compose的dsl很形象地描述了UI的视图结构,其背后对应这一视图树的结构体,我们称之为Composition。Composition在Composable初次执行时被创建,在Composable中访问State时,Composition记录其引用,当State变化时,Composition触发对应的Composable进行重组,更新视图树的节点,显示中的UI得到刷新。
Composable的生命周期
- OnActive(添加到视图树)
即Composable被首次执行,在视图树上创建对应的节点。 - OnUpdate(重组)
Composable跟随重组不断执行,更新视图树上的对应节点。 - OnDispose(从视图树移除)
Composable不再被执行,对应节点从视图树上移除。
Composable的副作用
Composable在执行过程中,凡是会影响外界的操作都属于副作用,比如弹出Toast、保存本地文件、访问远程或本地数据等。我们知道,重组可能会造成Composable反复执行,副作用显然是不应该跟随重组反复执行的。为此,Compose提供了一系列副作用API,可以让副作用API只发生在Composable生命周期的特定阶段,确保行为的可预期性。
-
DisposableEffect
该api可以感知composable的onactive和ondispose,允许通过副作用完成一些预处理和收尾处理。DisposableEffect想rememner一样可以接受观察参数key,但是它的key不能为空。如果key为Unit或true这样的常量,则block只在onactive时执行一次;如果key为其他变量,则block在onactive以及参数变化时的onupdate中执行。DisposableEffect的最后必须跟随一个onDispose代码块,否则会编译错误。 onDispose常用来做一些副作用的收尾处理。当有新的副作用会执行onDispose,此外当Composable进入onDispose时,也会执行。
kotlin@Composable fun DisposableEffectTest( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val inputText = remember { mutableStateOf("") } Log.e("DisposableEffectTest","Composed") DisposableEffect(inputText.value) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { Log.e("DisposableEffectTest","ON_START") } else if (event == Lifecycle.Event.ON_STOP) { Log.e("DisposableEffectTest","ON_STOP") } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { Log.e("DisposableEffectTest","onDispose") lifecycleOwner.lifecycle.removeObserver(observer) } } Scaffold() { Column(modifier = Modifier.padding(it)) { Button(onClick = { inputText.value = "按了一下" }) { Text(text = "按钮"+inputText.value) } } } }
-
SideEffect
SideEffect在每次成功重组时都会执行,所以不能用来处理那些好事或者异步的副作用逻辑。因为SideEffect能够获取与Composable一致的最新状态,它可以用来将当前State正确地暴露外外部。
kotlin@Composable fun rememberAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { /* ... */ } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect {//将状态通知外部 analytics.setUserProperty("userType", user.userType) } return analytics }
-
LaunchedEffect
当副作用中有处理异步任务的需求时,可以使用LaunchedEffect 。在Composable进入onactive时,LaunchedEffect会启动协程执行block中的内容,可以在其中启动子协程或调用挂起函数。当Composable进入OnDispose时,协程会自动取消,因此LaunchedEffect不需要实现OnDispose{...}。LaunchedEffect支持观察参数key的设置,当key发生变化时,当前协程自动结束,同时开启新协程。
c@Composable fun LaunchedEffectTest() { val state = remember { mutableStateOf("xiaomi") } LaunchedEffect(state){ Log.e("LaunchedEffectTest", "request") delay(3000)//模拟网络操作 state.value = "oppo" } Log.e("LaunchedEffectTest", state.value) Scaffold{ Column(modifier = Modifier.padding(it)) { Spacer(modifier = Modifier.padding(top = 50.dp)) Button(onClick = { state.value = "vivo" }) { Text(text = "按钮") } Spacer(modifier = Modifier.padding(top = 100.dp)) Text(text = "手机品牌 ${state.value}") } } }
-
rememberCoroutineScope
LaunchedEffect只能在Composable中调用,如果想在非Composable环境中使用协程,例如在Button的Onclick中使用协程,并希望其在OnDispose时自动取消,此时可以使用rememberCoroutineScope。
c@Composable fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { Column { /* ... */ Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { //Thread.sleep(1000) scaffoldState.snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
-
rememberUpdatedState
LaunchedEffect会在参数key变化时启动一个协程,但有时我们并不希望协程中断,只要能够实时获取最新状态即可,此时可以借助rememberUpdatedState实现。
c@Composable fun UpdatedStateTest() { var message= remember { mutableStateOf("start") } Scaffold { innerPadding -> Column(modifier = Modifier.padding(innerPadding)){ Button( onClick = { message.value = "clicked" } ) { Text("描述信息") } LoadingScreen(message.value) } } } @Composable fun LoadingScreen(text: String,scaffoldState: ScaffoldState = rememberScaffoldState()) { val messageText by rememberUpdatedState(text) Log.e("LoadingScreen", "start") LaunchedEffect(true) { Log.e("LoadingScreen", "delay origin ${messageText}") delay(4000) Log.e("LoadingScreen", "delay remember ${messageText}") scaffoldState.snackbarHostState.showSnackbar( message = "切换了方法", actionLabel = messageText ) } Scaffold(scaffoldState = scaffoldState) { Column(modifier = Modifier.padding(it)) { } } }
-
snapshotFlow
LaunchedEffect中可以通过rememberUpdatedState获取最新状态,但是当状态发生变化时,LaunchedEffect无法第一时间受到通知,如果通过改变观察参数key来通知状态变化,则会中断当前执行中的任务,成本太高。简言之,LaunchedEffect缺少轻量级的观察状态变化的机制。
c@Composable fun SnapshotFlow() { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { val listState = rememberLazyListState() LazyColumn(state = listState) { items(500) { index -> Text(text = "Item: $index") } } Log.e("SnapshotFlow", "Recompose") LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 4 } .distinctUntilChanged() .filter { it } .collect {//经过snapshotFlow 转换的flow是个冷流,只有在collect之后,block才开始执行。 Log.e("SnapshotFlow", "snapshotFlow${it}") } } } }
当一个LaunchedEffect中依赖的State会频繁变化时,不应该使用State的值作为key,而应该将State本身作为key,然后在其内部使用snapshotFlow 依赖状态。使用state作为key是为了当state对象本身变化时重启副作用。
-
produceState
produceState会启动一个协程,和SideEffect相反,使用此协程可以将非Compose状态转换为Compose状态。
c@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository ): State<Result<ImageBitmap>> { Log.e("ProduceStateExample", "loadNetworkImage: invoke" ) //当url和imageRepository发生变化时,producer会重新执行 return produceState<Result<ImageBitmap>>(initialValue = Result.Loading,url, imageRepository) { // value = Result.Loading val image = imageRepository.loadNetworkImage(url) //value 为 MutableState 中的属性 value = if (image == null) { Result.Error } else { Result.Success(image) } } } //密封类 sealed class Result<T>() { object Loading : Result<ImageBitmap>() object Error : Result<ImageBitmap>() data class Success(val image: ImageBitmap) : Result<ImageBitmap>() }
-
derivedStateOf
derivedStateOf用来将一个或多个State转成另一个State。derivedStateOf{...}的block中可以依赖其他State创建并返回一个DerivedState,当block依赖的State发生变化时,会更新此DerivedState,依赖此DerivedState的所有Composable会因其变化而重组。
c@Composable fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) { val todoTasks = remember { mutableStateListOf<String>("huawei", "xiaomi", "oppo", "apple", "Compose") } // 选择 todoTasks中 属于 highPriorityKeywords 的部分 val highPriorityTasks by remember(highPriorityKeywords) { derivedStateOf { todoTasks.filter { highPriorityKeywords.contains(it) } } } Log.e("TodoList", "todoTasks:${todoTasks.toList().toString()}" ) Log.e("TodoList", "highPriorityTasks:${highPriorityTasks.toList().toString()}" ) Column(modifier = Modifier.fillMaxWidth()) { LazyColumn { item { Text(text = "add-TodoTasks", Modifier.clickable { todoTasks.add("Review") }) } item { Divider( color = Color.Red, modifier = Modifier .height(2.dp) .fillMaxWidth() ) } items(highPriorityTasks) { Text(text = it) } item { Divider( color = Color.Red, modifier = Modifier .height(2.dp) .fillMaxWidth() ) } items(todoTasks) { Text(text = it) } } } }
derivedStateOf只能监听block内的state,一个非state类型数据的变化则可以通过remember的key进行监听,如上例所示。
注意:在statefulcomposable中创建状态时,需要使用remember包裹,状态只在onactive时创建一次,不跟随重组反复创建,所以remember本质上也是一种副作用api。 -
副作用API的观察参数
不少副作用api都允许指定观察参数key。当观察参数变化时,执行中的副作用会终止,key的频繁变化会影响执行效率。反之,如果副作用中存在可变值,但没有指定为key,有可能因为没有及时响应变化而出现bug。因此,关于参数key的添加可以遵循以下原则:当一个状态的变化需要造成副作用终止时,才将其添加为观察参数key,否则应该将其使用rememberUpdatedState包装后,在副作用中使用,以避免打断执行中的副作用。