可组合函数应该是无副作用的,但是当需要改变应用的状态时, 可组合函数需要从受控的环境调用,这个环境能够感知可组合函数的生命周。在这篇文章中,你会学习到可组合函数的生命周期与JetPack Compose提供的不同的副作用API。
可组合函数的生命周期
一个组合描述了App的UI界面,通过运行可组合函数生成。一个组合就是一个由可组合函数构成的树状结构,这个结构对应与UI界面。
当Jetpack Compose 框架第一次运行可组合函数时,在初始化组合期间,框架会追踪你调用的可组合函数。当APP的状态发生改变,Jetpack Compose 框架会安排一个重组。重组就是当JC框架重新执行可组合函数来响应状态改变,然后更新组合来反应任何的改变。
一个组合可以由一个初始组合产生,并且会
合成只能通过初始合成来生成,并且只能通过重组来更新。 修改合成的唯一方法是通过重组。
重组的一般触发条件就是StateM<T>状态的改变,Compose 会追踪组合中的这些状态,并且运行与其相关的可组合函数,和任何不可跳过的可组合函数。
注意:
可组合函数的生命周期与View、activity、fragment的生命周期相似,当一个可组合函数需要与外部资源进行交互,那么就会产生比较复杂的生命周期,那么你需要使用effcts
如果一个组合函数被调用多次,那么组合中就会包含多个实例。在组合中,每个调用都有自己的生命周期。
@Composable
fun MyComposable() {
Column {
Text("Hello")
Text("World")
}
}
这个图代表了MyComposable这个UI组合。不同的颜色代表不同的实例。
组合中的可组合函数剖析
组合中的一个可组合函数实例通过调用入口(call site)标识,Compose框架编译器认为每个调用入口都是不同的。在组合中,从多个调用入口调用可组合函数将会创建多个可组合函数的实例。
关键词:调用入口(call site)的意思是源码中可组合函数(composable)被调用的位置,调用位置会影响可组合函数在组合中的位置,同理也会影响UI树的结构。
如果在一次重组期间,一个可组合函数调用与上次重组不同的可组合函数,Compose框架将会识别哪个可组合函数被调用或没有被调用,也会标识那些在两次组合中都调用的组合函数,如果都被调用的函数的输入并没有发生变化,Compose框架会避免去重新组合他们。
保存标识(Preserving identity)对于将副作用(side effects )和他们的可组合函数关联起来至关重要,其目的是不用每次重组都重新调用一遍重组函数。
以下面的例子为例
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
@Composable
fun LoginInput() { /* ... */ }
在上面的代码片段中,LoginScreen将会有条件的去调用LoginError可组合函数,并且会一直调用LoginInput重组函数。每个调用都有唯一的调用入口和源码位置,编译器将会利用他们去做唯一标识。
这个图表示,当组合因为状态改变发生重组,LoginScreen没有发生重组,同一个颜色代表同一个实例。
尽管LoginInput在初始化和状态改变后都被调用了,但初始化生成的LoginInput实例在重组期间都不会发生改变,因为LoginInput并没有任何参数发生了变化,重组时LoginInput重组函数的调用将会被Compose跳过。
添加额外的信息帮助更智能的重组
多次调用可组合函数会降可组合函数多次加入到组合中,当从同一个调用入口多次调用可组合函数,Compose没有任何信息来唯一标识每次调用的可组合函数,所以除了调用入口,执行顺序就会被用来作为唯一标识。有时需要这种操作,但在某些情况下这可能导致不想要的行为。
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
// MovieOverview composables are placed in Composition given its
// index position in the for loop
MovieOverview(movie)
}
}
}
在上面的例子中,Compose使用执行顺序加调用入口来区分组合中不同的实例。如果一个新的movie被添加到列表的底部,Compose框架可以复用组合中已经存在的实例,因为他们的位置在列表中没有改变。
上图表示插入一个新的movie到列表中,相同的颜色代表相同的实例,也是就重新组合后以前的实例没有发生改变。
但是,如果movies列表发生了改变,无论是在在顶部,还是中间插入数据,或者移除、重新排序数据,都会导致整个MovieOverview的重组。这种情况是必须要注意的,例如如果MovieOverview正在通过副作用获取movie的图片,这个时候如果重组发生了,加载图片的动作讲过被取消然后重新开始。
@Composable
fun MovieOverview(movie: Movie) {
Column {
// Side effect explained later in the docs. If MovieOverview
// recomposes, while fetching the image is in progress,
// it is cancelled and restarted.
val image = loadNetworkImage(movie.url)
MovieHeader(image)
/* ... */
}
}
当在列表顶部插入一条数据,MovieOverview可组合函数不能被复用,所有的副作用将重启。不同的颜色代表不同的实例。
理想情况下,我们想将MovieOverview实例的标识与传入的movie的标识联系起来。如果我们重拍下movies列表,理想情况下,我们会重排序组合树中的实例,并不是用不同的实例去重新组合。Compose提供了一种由开发人员自己决定使用什么作为唯一标识的方法,key 可重组函数。
通过调用key 可组合函数包裹一个代码块,来传入一个或多个值。这些值将被混合使用作为组合中实例的唯一标识。一个key的值不需要全局唯一,只需要在可组合函数的调用入口范围内唯一。所以,在这个例子中,每个movie需要一个key来标识movies。
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
如上面代码所示,即使列表中的元素发送了变化,Compose也可以识别出那些变化的,并可以复用没有变化的实例。
标识一个新的元素插入到列表中,Compose可以复用movie.id没有变化的,而且他们的副作用还是可以继续执行。
关键:
使用key可重组函数帮助Compose标识组合中的可重组实例,这对于多个可重组函数在同一个调用入口调用,并且还包含副作用或者内部状态是重要的。
一些可组合函数内置支持key可组合函数,例如LazyColumn。
@Composable
fun MoviesScreen(movies: List<Movie>) {
LazyColumn {
items(movies, key = { movie -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
如果输入没有变化就跳过
如果一个可组合函数已经在组合中,当所有的输入都是稳定的并且没有改变,这个可组合函数将跳过重组。
一个稳定的类型必须满足如下的条件:
- 对于两个相同的实例的equals的结果必须永远相同
- 如果可组合函数的一个public属性改变了,Composition将会被通知到
- 所有的public属性类型也都是稳定的
有许多重要的通用类型符合上面的条件,编译器就像对待@Stable注解一样对待这些类型,即使他们没有显式声明@Stable。
- 所有的原始类型 :Boolean ,Int ,Long,Float,Char 等
- String
- 所有的函数类型(lambadas)
这些类型都符合@Stable,因为他们是不可变的,因为不可变类型是永远不可变的。
一个值得注意的类型尽管是stable的,但是是可变的,就是Compose的MutableState类型。如果一个值在MutableState中,状态对象总体被认为是稳定的,因为如果State的.value属性发生任何更改,都会通知Compose。
当作为参数传递给可组合对象的所有类型都稳定时,将根据UI树中可组合位置对参数值进行相等性比较。 如果自上次调用以来所有值均未更改,则跳过重新组合。
关键点:
如果所有输入稳定且未更改,则Compose跳过可组合对象的重组。 比较使用equals方法。
Compose仅在可以证明类型的情况下才认为它是稳定的。 例如,接口通常被视为不稳定,而具有可变的公共属性(其实现可能是不可变的)的类型也不是稳定的。
如果Compose无法推断出某个类型是稳定的,但您想强制Compose将其视为稳定,请使用@Stable注解对其进行标记。
// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
在上面的代码片段中,由于UiState是接口,因此Compose通常可以认为此类型不稳定。 通过添加@Stable批注,您可以告诉Compose这种类型是稳定的,从而允许Compose支持智能重组。 这也意味着,如果将接口用作参数类型,则Compose会将其所有实现视为稳定。
状态和effect使用例子
可组合函数应该是无副作用的,当你需要改变APP的状态,您应该使用Effect API,以便以可预测的方式执行那些副作用。
关键字:一个effect,就是一个可组合函数,这个可组合函数不生成UI,而是在组合完成时产生副作用。
由于在Compose中打开效果的可能性不同,因此很容易过度使用它们,确保您在其中进行的工作与UI相关并且不会中断单向数据流
注意:响应式UI本质上是异步的,Jetpack Compose通过在API级别上包含协程而不是使用回调来解决此问题。
LaunchedEffect: 在某个可组合项的作用域内运行挂起函数
为了可以在可重组函数中安全得调用挂起函数,需要用LaunchedEffect可重组函数。当LaunchedEffect进入组合(Composition)时,它将启动协程,并将代码块作为参数传递。 如果LaunchedEffect离开组合,协程将被取消。如果使用不同的键重组LaunchedEffect(请参见下面的Restarting Effects部分),则现有的协程将被取消,新的挂起函数将在新的协程中启动。
例如,在S中显示Snackbar是通过SnackbarHostState.showSnackbar函数完成的,该函数是一个挂起函数