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包装后,在副作用中使用,以避免打断执行中的副作用。

相关推荐
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王7 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡7 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道8 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库9 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道9 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe9 小时前
Android Hook - 动态加载so库
android
居居飒10 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
晓纪同学13 小时前
QT创建一个模板槽和信号刷新UI
开发语言·qt·ui