《Jetpack Compose系列学习》-4 Compose状态和生命周期

之前我们介绍了Compose编程思想,今天我们来看看Compose的状态和生命周期,先来看看Compose的状态。

Compose中的状态

先讲一个概念————组合,之前说过可组合函数,而组合用于描述界面,通过运行可组合项来生成,也是树的结构。

简单说下Compose的工作流程:在初始组合期间,Compose跟踪为描述界面而调用的可组合项;当应用程序的状态发生变化时,Compose会安排重组;重组过程中会运行可能已更改的可组合项以响应状态变化,然后Compose会更新组合以反映所有的更改,这就是Compose的工作流程。这里需要注意:组合只能通过初试组合生成且只能通过重组进行更新。修改组合的唯一方式就是重组。

知道了Compose的工作流程,我们来看看Compose的状态,先看一个例子:

@Compoable
fun TestState() {
    Column(modifer = Modifier.fillMaxSize()) {
        var index = 0
        Button(onClick = {
            index ++
            Log.e("LM" , "TestState: $index")
        }) {
            Text("Add")
        }
        Text("$index", fontSize = 30.sp)
    }
}
复制代码

代码很简单,就是想每次点击按钮,数字增加并显示在Text上。但是上面的代码能按照预期正常执行吗?答案是否定的,why?其实之前已经介绍到了,修改组合的唯一方式是重组,但是上面的代码并不能触发Compose执行重组。那么怎么才能触发Compose执行重组呢?那就必须用到Compose的状态。

需要引入本地状态来保存应该显示的index,使用remember{mutableStateOf()}传入index的默认值,这样每当index的状态改变,Text显示的值才发生变化,代码改成如下方式:

@Composable
fun TestState2() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val index = remember {mutableStateOf(0)}
        Button(onClick = {
            index.value ++
            Log.e("LM" , "TestState: ${index.value}")
        }) {
            Text("Add")
        }
        Text("${index.value}", fontSize = 30.sp)
    }
}
复制代码

可组合函数可以使用remember可组合项记住单个对象,系统会在初始组合期间将由remember计算的值存储在组合中,并在重组期间返回存储的值。remember可以存储可变对象和不可变对象。mutableStateOf会创建MutableState,MutableState是Compose中的可观察类型,在MutableState的值有任何更改的情况下,系统会安排重组读取此值的所有可组合函数,以实现重组。

remember可以在重组后保持状态。如果在未使用remember的情况下使用mutableStateOf,每次重组可组合项的时候,系统都会将状态重新初始化为默认值。虽然remember可在重组后保持状态,但不会在配置更改后保持状态,比如旋转屏幕或者来电之后系统就会将状态重新初始化为默认值。所以这时使用remember就不行了,而需要使用rememberSaveable.它会自动保存可保存在Bundle中的任何值。对于其他类型的值,可以通过序列化之后进行保存。有点类似于Activity中的onSaveInstanceState方法,用法如下:

@Composable
fun TestState3() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val index = rememberSaveable {mutableStateOf(0)}
        Button(onClick = {
            index.value ++
            Log.e("LM" , "TestState: ${index.value}")
        }) {
            Text("Add")
        }
        Text("${index.value}", fontSize = 30.sp)
    }
}
复制代码

如果某个可组合项保持自己的状态,就会变得难以复用和测试,同时该可组合项与其状态的存储方式也会紧密关联。应该将此可组合项改为无状态可组合项,即不保持任何状态的可组合项。

为此,我们可以使用状态提升。它也是一种编程模式,我们可以将可组合项的状态移至该可组合项的调用方。一种简单的方式是使用参数替换状态,同时使用lambda表示事件。下面我们看看怎样提升上面示例代码的状态:

@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(onClick = {
            onIndexChange(index + 1)
        }) {
            Text("Add")
        }
        Text("$index", fontSize = 30.sp)
    }
}

@Compoable
fun TestState4() {
    val index = rememberSaveable {mutableStateOf(0)}
    TestState4(index.value) { index.value = it }
}
复制代码

状态提升之后,代码变得更容易测试,重复使用也变得简单了,其实这就是方法的重载,方便调用而已。

ViewModel和状态

在Compose中,可以使用ViewModel公开可观察存储器(如LiveData或Flow)中的状态,还可以使用它处理影响相应状态的事件。上面的例子中的TestState也可以使用ViewModel来实现:

class TestViewModel: ViewModel() {
    private val _index = MutableLiveData(0)
    val index: LiveData<Int> = _index
    
    fun onIndexChange(newName: Int) {
        _index.value = newName
    }
}

@Compoable
fun TestState5(testViewModel: TestViewMOdel = viewModel()) {
    val index by testViewModel.index.observeAsState(0)
    TestState4(index) { testViewModel.onIndexChange(it) }
}

@Composable
fun TestState4(index: Int, onIndexChange: (Int) -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(onClick = {
            onIndexChange(index + 1)
        }) {
            Text("Add")
        }
        Text("$index", fontSize = 30.sp)
    }
}
复制代码

observeAsState可观察LiveData并返回State对象,每当LiveData发生变化,该对象都会更新。State是Compose可以直接使用的可观察类型,前面提到的MutableState就是可变的State。

Compose生命周期

可组合项应该没有附带效应,但是如果转变应用程序状态时需要使用可组合项,应从能感知可组合项生命周期的受控环境中调用这些可组合项。可组合项的生命周期很简单,通过以下事件定义:进入组合,执行0次或多次重组,然后退出组合。

如果某一可组合项被调用多次,在组合中将放置多个实例。每次调用时,可组合项在组合中都有自己的生命周期,我们看下面这个例子理解下可组合项的生命周期:

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}
复制代码

Column(纵向线性布局)中包裹两个Text控件,在组合中

image.png 如果某一可组合项被调用多次,在组合中将放置多个实例,如果某一元素具有不同颜色,则表明它是一个独立实例。

状态和效应用例

我们知道可组合项应该没有附带效应。如果需要改变应用程序的状态,则需要使用Effect API,以可预测的方式应用这些附带效应。这里的Effect就是我们说的“效应”。它的作用有:

1.在某个可组合项的作用域中运行挂起函数:

如果需要从可组合项内安全调用挂起函数,可以使用LaunchedEffect可组合项,当LaunchedEffect进入组合时,它会启动一个协程,并将代码块作为参数传递,如果LaunchedEffect退出组合,协程将取消,代码如下:

@Composable
fun EffectScreen(
    state: Result<String>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    if (state.isFailure) {
        LaunchedEffect(scaffoldState) {
            scaffoldState.snackbarHostState.showSnackbar {
                message = "Error message",
                actionLabel = "Retry message"
            }
        }
    }
    
    Scaffold(scaffoldState = scaffoldState) {
        // ...
    }
}
复制代码

如果上面代码中的State包含错误,则会触发协程;如果没有错误,则取消协程。由于LaunchedEffect调用点在if中,因此当该语句为false,如果LaunchedEffect包含在组合中,则它会被移除,所以协程将被取消。

2.获取组合感知作用域,在可组合项外启动协程

由于LaunchedEffect是可组合函数,所以只能在其它可组合函数中使用,为了在可组合项外启动协程,但其作用域已确定,以便协程在退出组合后自动取消,可以使用rememberCoroutineScope。此外,如果需要手动控制一个或多个协程的生命周期,也可以使用rememberCoroutineScope,例如在用户事件发生时取消动画。rememberCoroutineScope是一个可组合函数,会返回一个coroutineScope,该coroutineScope绑定到调用它的组合点,调用退出组合后,作用域将取消。

3.在值更改时不重启

当其中一个参数发生变化时,LaunchedEffect就会重启,但在某些情况下,我们希望在效应中捕获某个值,但如果该值发生变化,不希望效应重启。所以我们可以使用rememberUpdatedState来创建对可捕获和更新的该值的引用。因此重新创建和重启这些操作可能代价高昂或令人望而却步。

4.需要清理的效应

对于需要在键发生变化或可组合项退出组合后进行清理的附带效应,可以使用DisposableEffect。如果DisposableEffect键发生变化,可组合项需要处理其当前效应,并通过再次调用效应进行重置。

5.将Compose状态发布为非Compose代码

如需与非Compose管理的对象共享Compose状态,可以使用SideEffect可组合项,因为每次成功重组时都会调用该可组合项。

6.将非Compose状态发布为Compose代码

produceState会启动一个协程,该协程将作用域限定为可将值推送到返回的State组合。使用此协程将非Compose状态转换为Compose状态,如将外部订阅驱动的状态(LiveData、RxJava或Flow)引入。即使produceState创建了一个协程,它也可以用于观察非挂起的数据源。如果需要移除对该数据源的订阅,可以使用awaitDispose函数。

7.将一个或多个状态对象转换为其它状态

如果某个状态是从其它状态对象计算得出的,可以使用derivedStateOf,可以确保仅当计算中使用的状态之一发生变化时才会进行计算。

重启效应

我们上面介绍了Compose中有一些效应,如LaunchedEffect、produceState或DisposableEffect,会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新效应。调用API的形式:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
复制代码

一般来说,效应代码块中使用的可变变量和不可变变量应作为参数添加到效应可组合项中,除此之外,可以添加更多参数,以便在效应重启时强制执行。如果更改变量不应导致效应重启,则应该将变量封装在rememberUpdatedState中。如果由于变量封装在一个不含键的remember中,使之没有发生变化,则无须将变量作为键传递给效应。我们来看看一个DisposableEffect使用的例子:

@Composable
fun Backhandler(
    backDispatcher: OnBackPressedDispatcher, 
    onBack: () -> Unit) {
    
    val backCallback = remember {// ...}
    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            backCallback.remove()
        }
    }
}
复制代码

在上面展示的DisposableEffect代码中,效应将其中使用的backDispatcher作为参数,因为对它们的任何更改都会导致效应重启。

无须使用backCallback作为DisposableEffect键,因为它的值在组合中绝对不会发生变化;它封装在不含键的remember中。如果未将backDispatcher作为参数传递,并且该代码发生了变化,那么BackHandler将重组,但DisposableEffect不会进行处理和重启。这会引起问题,因为此后会使用错误的backDispatcher.可以使用true等常量作为效应键,使其遵循调用点的生命周期。实际上,它具有有效的用例,如上面的LaunchedEffect实例;但是建议暂停使用这些用例,并确保那是必要的内容操作。

今天的学习内容有点抽象,但后面通过简单控件介绍使用和Demo的实现,会对这块慢慢有进一步的了解。

猜你喜欢

转载自juejin.im/post/7074190120303198244