Android全新UI框架之Compose状态管理与重组

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有三种用法:

  1. 创建MutableState

    kotlin 复制代码
     var counter:MutableState<Int> = mutableStateOf(0)
  2. 解构方式

    kotlin 复制代码
      val(counter,setCounter) = mutableStateOf(0)

    此时的counter已经是一个Int类型的数据,后续使用的地方可以直接访问,无须再使用点操作符获取value,而需要更新counter的地方可以使用setCounter(xx)完成。

  3. 属性代理

    使用by关键字直接获取Int类型counter。

    kotlin 复制代码
    var 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在重组执行时的一些特性,避免陷入重组的"陷阱"。

  1. Composable会以任意顺序执行

    多个composable函数未必按照先后顺序执行,因此不能在composable设置一个全局变量,以期望在第一个composable中修改该值,在第二个composable修改为另一个值。

  2. Composable会并发执行

    重组中的Composable并不一定执行在UI线程,它们可能在后台线程池中并行执行,这有利于发挥多核处理器的性能优势。

  3. Composable会反复执行

    除了重组会造成Composable的再次执行外,在动画等场景中每一帧的变化都可能引起Composable的执行,因此Composable有可能会短时间内反复执行,我们无法准确判断它的执行次数。

  4. 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生命周期的特定阶段,确保行为的可预期性。

  1. 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)
                }
            }
        }
    }
  2. 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
    }
  3. 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}")
            }
        }
    }
  4. 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")
                }
            }
        }
    }
  5. 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)) {
                
            }
        }
    }
  6. 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对象本身变化时重启副作用。

  7. 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>()
    }
  8. 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。

  9. 副作用API的观察参数

    不少副作用api都允许指定观察参数key。当观察参数变化时,执行中的副作用会终止,key的频繁变化会影响执行效率。反之,如果副作用中存在可变值,但没有指定为key,有可能因为没有及时响应变化而出现bug。因此,关于参数key的添加可以遵循以下原则:当一个状态的变化需要造成副作用终止时,才将其添加为观察参数key,否则应该将其使用rememberUpdatedState包装后,在副作用中使用,以避免打断执行中的副作用。

相关推荐
阿巴斯甜20 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker21 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android