Life cycle and side effects of Android Jetpack Compose

1 Overview

With the help of Kotlin's DSL language features, Compose can vividly describe the view structure of the UI. The view structure corresponds to the data structure of a view tree. This tree is called Composition in Compose. Composition will be executed when Composable is first executed. Create, when accessing State in Composable, Compose records its reference. When State changes, Composition triggers the corresponding Composable to reorganize, updates the nodes in the view tree, and then refreshes the UI. We all know that Android's Activity will call back the corresponding life cycle in different scenarios, so will Compose's Composition tree also have similar callbacks when it is updated? The answer is yes, but it is different from the Activity's life cycle callback. Differences, this article will introduce the life cycle of Compose and a new concept of side effects.

2.Composeable life cycle

We already know that the execution of the Composable function will result in a view tree. Each Composable component corresponds to a node on the tree. The life cycle of Composable can be defined around the addition and update of these nodes on the view tree. As shown below:

Insert image description here
As shown in the figure above, the Composable life cycle has three callbacks, namely onActive, OnUpdate, and OnDispose (the first letter of the life cycle name in the figure is lowercase, but the meaning is exactly the same). Their meanings are as follows:

OnActive: Add the node to the view tree, that is, Composable is executed for the first time, and create the corresponding node on the view tree.
OnUpdate: Reorganization, that is, Composable continues to execute following the reorganization, updating the corresponding node on the view tree.
OnDispose: Remove the node from the view tree. , that is, Composable is no longer executed, and the corresponding node is removed from the view tree.

It should be noted that there is a difference between the life cycle of Composable and the life cycle of Activity. The role of Composable is more similar to the traditional view, so it does not have the concept of front and back switching like Activity or Fragment. The life cycle is relatively Simple, although in a Compose project, Composable will also be used to host the page. When the page is no longer displayed, it means that the Composable node is also destroyed immediately. It will not save the instance in the background like Activity or Fragment, so even if we put Composable When used as a page, there is no concept of switching between front and back.

3.Compose side effects and API

What is a side effect? ​​As the name suggests, it sounds like a pretty bad thing. This is indeed the case. During the execution of Composable, some operations will affect the outside world. These operations are called side effects. This concept also exists in Vue.js. For example, there is a global variable referenced by two Composables. When the global variable is modified in one Composable, the other Composable will be affected. This is called a side effect. In addition, popping up Toast, saving local files, remotely accessing local data, etc. are all side effects. Because Composable reorganization will be executed frequently and repeatedly, it is obvious that side effects should not be executed repeatedly following the reorganization.因此Compose提供了一系列的副作用API。这些API可以让副作用只发生在Composable生命周期的特定阶段,确保行为的可预期性。

3.1.Compose Side Effect API

3.1.1 DisposableEffect

DisposableEffect can sense Compoable's onActive and onDispose, and we can complete some preprocessing and finishing processing through the side effect API. For example, the following example of registration and logout system return keys:

    @Composable
    fun HandleBackPress(enabled: Boolean = true, onBackPressed: () -> Unit) {
    
    
        val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
    
    
            "No LocalOnBackPressedDispatcherOwner provided!!!"
        }.onBackPressedDispatcher

        val backCallback = remember {
    
    
            object : OnBackPressedCallback(enabled) {
    
    
                override fun handleOnBackPressed() {
    
    
                    onBackPressed()
                }
            }
        }

        DisposableEffect(backDispatcher) {
    
    
            backDispatcher.addCallback(backCallback)
            onDispose {
    
    
                backCallback.remove()
            }
        }
    }

In the above code, remember creates an OnBackPresedCallBack callback event that returns the key. The reason why the remember package is used is to avoid it being created repeatedly during reorganization. So we can also treat remember as a side effect API
and then register the return key event callback in OnBackPressedDispatcher in the statement block after DisposableEffect. DisposableEffect, like remember, can receive an observation parameter key, but this key cannot be empty. Then its execution is as follows:
If the key is a constant such as Unit or true, the statement block after DisposableEffect will only be executed once in OnActive
. If the key is other variables, the statement block after DisposableEffect will be in OnActive and OnUpdate when the parameters change. Execution, for example, in the above example code: assuming that when backDispatcher changes, the statement block after DisposableEffect will be executed again, and a new backCallback callback will be registered. If backDispatcher does not change, the statement block after DisposableEffect will not be reorganized.

DisposableEffect{...} must be followed by an onDispose code block at the end, otherwise a compilation error will occur. OnDispose is often used to do some finishing work for side effects, such as logging off callbacks to avoid leaks.

新的副作用到,即DisposableEffect因为key的变化再次执行,参数key也可以是代表一个副作用的标识

3.1.2 SideEffect

SlideEffect is executed on every successful reorganization, so it cannot be used to handle time-consuming or asynchronous side-effect logic. The difference between SlideEffect and Composable is that reorganization will trigger Composable to re-execute, but the reorganization may not necessarily end successfully, and some reorganizations may fail midway. SlideEffect will only be executed when the reorganization is successful. Use an example to introduce the usage of SlideEffect, as follows:

@Composable
fun TestSlideEffect(touchHandler:ToucheHandler){
    
    
val drawerState = rememberDrawerState(DrawerValue.Closed)
SlideEffect{
    
    
	touchHandler.enable = drawerState.isOpen
}

As shown in the code above: when the drawerState state changes, the latest state will be notified to the external ToucheHandler. If it is not placed in SlideEffect, an error state may be transmitted when the reorganization fails.

3.2 Compose asynchronous processing of side effects API

3.2.1 LaunchedEffect

LaunchedEffect can be used when there is a need to handle asynchronous tasks in side effects. When Composable enters OnActivite, LaunchedEffect will start the coroutine to execute the content in the statement block, where you can start a sub-coroutine or call a suspension function. When Composable enters OnDispose, the coroutine will be automatically canceled, so there is no need to implement OnDispose{} in LuanchedEffect.

LaunchedEffect supports observing the parameter Key. When the key changes, the current coroutine automatically ends and a new coroutine is started. Sample code looks like this:

  @Composable
    fun LaunchedEffectDemo(
        state:UiState<List<Movie>>,
        scaffoldState:ScaffoldState = rememberScaffoldState()
    ){
    
    
        if(state.hasError){
    
    
            LaunchedEffect(scaffoldState.snackbarHostState){
    
    
                scaffoldState.snackbarHost.showSnackbar(
                    message="Error",
                    actionLabel = "Retry Msg"
                )
            }
        }
        
        Scaffold(scaffoldState = scaffoldState){
    
    
            ...
        }
    }

注:代码仅供理解使用,无法直接运行

As shown in the above code, when the state contains an error, a SnackBar will be displayed, and the display of SnackBar requires a coroutine environment, which LaunchedEffect can provide. When scaffoldState.snackbarHostState changes, a new coroutine will be started and SnackBar will be displayed again. When state.hasError becomes false, LaunchedEffect will enter OnDispose, the coroutine will be canceled, and the SnackBar being displayed at this time will also disappear.
由于副作用通常都是在主线程执行的,所以遇到副作用中有耗时任务时,优先考虑使用LaunchedEffect API 处理副作用

3.2.2 rememberCoroutineScope

Although LaunchedEffect can start the coroutine, LaunchedEffect can only be called in Composable. If you want to use the coroutine in a non-Composable, such as using SnackBar in onClick{} of the Button component, and want to automatically cancel it on OnDispose. How should this be achieved? The answer is to use rememberCoroutineScope. rememberCoroutineScope will return a coroutine scope CoroutineScope, which can be automatically canceled when the current Composable enters OnDispose. An example looks like this:

    @Composable
    fun rememberCoroutineScopeDemo(scaffoldState:ScaffoldState = rememberScaffoldState()){
    
    
        val scope = rememberCoroutineScope()
        Scaffold(scaffoldState = scaffoldState){
    
    
            Column {
    
     
            ...
                Button(
                    onClick = {
    
    
                        scope.launch {
    
     
                            scaffoldState.snackbarHostState.showSnackBar("do something")
                        }
                    }
                ){
    
    
                    Text("click me")
                }
            }
        }
    }

注:代码仅供理解使用,无法直接运行

3.2.3 rememberUpdateState

We mentioned earlier that LaunchedEffect will start a coroutine when the parameter key changes, but sometimes we do not want the coroutine to be interrupted, so as long as the latest status can be obtained in real time, it can be achieved with the help of the rememberUpdateState API. . The code looks like this:

   @Composable
    fun RememberUpdateStateDemo(onTimeOut: ()->Unit){
    
    
        val currentOnTimeOut by rememberUpdatedState(onTimeOut)
        LaunchedEffect(Unit){
    
    
            delay(1000)
            currentOnTimeOut() // 这样总是能够取到最新的onTimeOut
        }
// 省略不重要的代码
    }

As shown in the code above, we set the parameter key of LaunchedEffect to Unit. Once the code block starts execution, it will not be interrupted due to the reorganization of RememberUpdateStateDemo. When currentOnTimeOut() is executed, the latest onTimeOut instance can still be obtained. This is guaranteed by the use of rememberUpdateState.
The implementation principle of rememberUpdateState is actually the combination of remember and mutableStateOf, as shown in the following figure:

Insert image description here

The picture above is a screenshot of the implementation of rememberUpdateState. We can see that remember ensures that instances of MutableState can exist across reorganizations. What is accessed in the side effect is actually the latest newValue in MutableState. Therefore we can conclude:rememberUpdateState可以在不中断副作用的情况下感知外界的变化

3.2.4 snapshotFlow

In the previous section, we learned that LaunchedEffect can obtain the latest status through rememberUpdateState, but when the status changes, LaunchedEffect cannot receive notifications at the first time. If the status change is notified by changing the observation parameter key, the current status will be interrupted. tasks to perform. So snapshotFlow appeared, which can convert the state into a Coroutine Flow. The code is as follows:

    @Composable
    fun SnapShotFlowDemo(){
    
    
        val pagerState = rememPagerState()
        LaunchedEffect(pagerState){
    
    
            // 将pageState转为Flow
            snapshotFlow {
    
     
                pagerState.currentPage
            }.collect{
    
    
                page->
                // 当前页面发生变化
            }
        }
    }

As shown in the above code, snapshotFlow internally subscribes to the status pageState of the tab page. When the tab is switched, the value of pageState changes and is notified to the downstream collector for processing. Although the pageState here is used as the observation parameter key of LaunchedEffect, the instance of pageState has not changed. The comparison based on equals cannot detect the change, so we don't have to worry about the coroutine being interrupted.

When snapshotFlow{} internally accesses State, it will subscribe to its changes through the "snapshot" system. When State changes, flow will send new data. If there is no change in State, it will not be sent. What needs to be noted here is that the Flow converted by snapshotFlow is a cold flow. Only after collect does the block start executing.

当一个LaunchedEffect中依赖的State会频繁变化时,不应该使用State的值作为key,而应该将State本身作为key,然后再LaunchedEffect内部使用snapshotFlow依赖状态,使用State作为key是为了当State对象本身变化时重启副作用

3.3 State creation side effects API

In previous studies, we have learned that when creating a state in a Stateful Composable, you need to use the remember package. The state is only created once during OnActive and will not be created repeatedly following the reorganization of the Composable, so remember is essentially a side effect API. In addition to remember, there are several other side-effect APIs used to create states, which will be introduced one by one next.

3.3.1 produceState

We have already learned SideEffect, which is often used to expose compose State to external use. The produceState introduced in this section is the opposite. It can convert an external data source into a State. This external data source can be an observable data such as LiveData or RxJava, or it can be any ordinary data type.
The usage scenario of produceState is as follows, from Chapter 4 of the book "Jetpack Compose from Getting Started to Practical Combat":

   @Composable
    fun loadImage(
        url:String,
        imageRepository:IMageRepository
    ) : State<Result<Image>> {
    
    
        return produceState(initialValue = Result.Loading,url,imageRepository){
    
    
            // 通过挂起函数请求图片
            val image = imageRepository.load(url)
            
            // 根据请求结果设置Result类型
            // 当Result变化时,读取此State的Composable触发重组
            value = if(image == null){
    
    
                Result.Error
            }else{
    
    
                Result.Success(image)
            }
        }
    }

As shown in the above code, we request an image through the network and use produceState to convert it to State<Result>. If the acquisition fails, an error message will be returned. produceState observes the two parameters of url and imageRepository. When they change, the producer will re-execute. . As shown below

As shown in the figure: the implementation of produceState is to use remember to create a MutableState, and then update it asynchronously in LaunchedEffect.
produceState 的实现给我们展示了如何利用remember与LaunchedEffect等API封装自己的业务逻辑并且暴露State.我们在Compose项目中,要时刻带着数据驱动的思想来实现业务逻辑。

3.3.2 derivedStateOf

derivedStateOf is used to convert one or more States into another State. The block of derivedStateOf{} can rely on other States to create and return a DerivedState. When the dependent State in the block changes, this DerivedState will be updated and depends on this DerivedState. All Composables will be reorganized as a result of their changes. First look at the following code:

@Composable
fun DerivedStateOfDemo() {
    
    
    val postList = remember {
    
     mutableStateListOf<String>() }
    var keyword by remember {
    
     mutableStateOf("") }

    val result by remember {
    
    
        derivedStateOf {
    
     postList.filter {
    
     it.contains(keyword, false) } }
    }

    Box(modifier = Modifier.fillMaxSize()) {
    
    
        LazyColumn {
    
    
            items(result.size) {
    
    
                // do something
            }
        }
    }
}

In the above code, a set of data is searched based on keywords, and the search results are displayed. The retrieval data and keywords are both mutable states, and we implement the retrieval logic inside the block of derivedStateOf{}. When postList or keyworld changes, the result will be updated. In fact, this function can also be implemented using remember. The code is as follows:

@Composable
fun DerivedStateOfDemo() {
    
    
    val postList by remember {
    
     
        mutableStateOf(emptyList<String>())
    }
    var keyword by remember {
    
     mutableStateOf("") }

    val result by remember(postList, keyword) {
    
    
        postList.filter {
    
     
            it.contains(keyword,false)
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
    
    
        LazyColumn {
    
    
            items(result.size) {
    
    
                // do something
            }
        }
    }
}

But if the above code is written like this, it means that as long as one of postList and keyworld changes, Composable will be reorganized. And we use derivedStateOf to trigger reorganization only when DerivedState changes.所以当一个结算结果依赖较多的State时,使用derivedStateOf有助于减少重组的次数,提高性能。

提示:不少的副作用API都允许指定观察参数key,例如LaunchedEffect、produceState、DisposableEffect等,当观察参数变化时,执行中的副作用会终止,key的频繁变化会影响执行效率。而假设副作用中存在可变值但是却没有指定key,就会出现因为没有及时响应变化而出现Bug,因此我们可以根据一个原则确定key的添加:当一个状态的变化需要造成副作用终止时,才将其添加为观察参数,否则应该将其使用rememberUpdateState包装后,在副作用中使用,以避免打断执行中的副作用。

Guess you like

Origin blog.csdn.net/zxj2589/article/details/133346567