Jetpack Compose(5)——生命周期與副作用函數(shù)
一、 Composable 的生命周期
Composable 組件都是函數(shù),Composable 函數(shù)執(zhí)行會得到一棵視圖樹,每一個 Composable 組件對應(yīng)視圖樹上的一個節(jié)點(diǎn)。Composable 的生命周期定義如下:
- onActive(添加到視圖樹) Composable 首次被執(zhí)行,即在視圖樹上創(chuàng)建對應(yīng)的節(jié)點(diǎn)。
- onUpdate(重組) Composable 跟隨重組不斷執(zhí)行,更新視圖樹上對應(yīng)的節(jié)點(diǎn)。
- onDispose(從視圖樹移除) Composable 不再被執(zhí)行,對應(yīng)節(jié)點(diǎn)從視圖樹上移除。
對于 Compose 編寫 UI 來說,頁面的變化,是依靠狀態(tài)的變化,Composable 進(jìn)行重組,渲染出不同的頁面。當(dāng)頁面可見時,對應(yīng)的節(jié)點(diǎn)被添加到視圖樹,當(dāng)頁面不可見時,對應(yīng)的節(jié)點(diǎn)從視圖樹移除。所以,雖然 Activity 有前后臺的概念,但是使用 Compose 編寫的頁面,對于 Composable 沒有前后臺切換的概念。當(dāng)頁面切換為不可見時,對應(yīng)的節(jié)點(diǎn)也被立即銷毀了,不會像 Activity 或者 Fragment 那樣在后臺保存實(shí)例。
二、 Composable 的副作用
上一篇將重組的文章講到,Composable 重組過程中可能反復(fù)執(zhí)行,并且中間環(huán)節(jié)有可能被打斷,只保證最后一次執(zhí)行的狀態(tài)時正確的。
試想一個問題,如果在 Composable 函數(shù)中彈一個 Toast ,當(dāng) Composable 發(fā)生重組時,這個 Toast 會彈多少次,是不是就無法控制了。再比如,在 Composable 函數(shù)中讀寫函數(shù)之外的變量,讀寫文件,請求網(wǎng)絡(luò)等等,這些操作是不是都無法得到保證了。類似這樣,在 Composable 執(zhí)行過程中,凡是會影響外界的操作,都屬于副作用。在 Composable 重組過程中,這些副作用行為都難以得到保證,那怎么辦?為了是副作用只發(fā)生在生命周期的特定階段, Compose 提供了一系列副作用函數(shù),來確保行為的可預(yù)期性。下面,我們看看這些副作用函數(shù)的使用場景。
2.1 SideEffect
SideEffect 在每次成功重組的時候都會執(zhí)行。
Composable 在重組過程中會反復(fù)執(zhí)行,但是重組不一定每次都會成功,有的可能會被中斷,中途失敗。 SideEffect 僅在重組成功的時候才會執(zhí)行。
特點(diǎn):
- 重組成功才會執(zhí)行。
- 有可能會執(zhí)行多次。
所以,SideEffect 函數(shù)不能用來執(zhí)行耗時操作,或者只要求執(zhí)行一次的操作。
典型使用場景,比如在主題中設(shè)置狀態(tài)欄,導(dǎo)航欄顏色等。
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
2.2 DisposableEffect
DisposableEffect 可以感知 Composable 的 onActive 和 onDispose, 允許使用該函數(shù)完成一些預(yù)處理和收尾工作。
典型的使用的場景,注冊與取消注冊:
DisposableEffect(vararg keys: Any?) {
// register(callback)
onDispose {
// unregister(callback)
}
}
這里首先參數(shù) keys 表示,當(dāng) keys 變化時, DisposableEffect 會重新執(zhí)行,如果在整個生命周期內(nèi),只想執(zhí)行一次,則可以傳入 Unit
而 onDispose 代碼塊則會在 Composable 進(jìn)入 onDispose 時執(zhí)行。
2.3 LaunchedEffect
LaunchedEffect 用于在 Composable 中啟動協(xié)程,當(dāng) Composable 進(jìn)入 onAtive 時,LaunchedEffect 會自動啟動協(xié)程,執(zhí)行 block 中的代碼。當(dāng) Composable 進(jìn)入 onDispose 時,協(xié)程會自動取消。
使用方法:
LaunchedEffect(vararg keys: Any?) {
// do Something async
}
同樣支持可觀察參數(shù),當(dāng) key 變化時,當(dāng)前協(xié)程自動結(jié)束,同時開啟新協(xié)程。
2.4 rememberCoroutineScope
LaunchedEffect 只能在 Composable 中調(diào)用,如果想在非 Composable 環(huán)境中使用協(xié)程,比如在 Button 的 OnClick 中開啟協(xié)程,并希望在 Composable 進(jìn)入 onDispose 時自動取消,則可以使用 rememberCoroutineScope 。
具體用法如下:
@Composable
fun Test() {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
// do something
}
}
) {
Text("click me")
}
}
DisposableEffect 配合 rememberCoroutineScope 可以實(shí)現(xiàn) LaunchedEffect 同樣的效果,但是一般這樣做沒有什么意義。
2.5 rememberUpdatedState
rememberUpdatedState 一般和 DisposableEffect 或者 LaunchedEffect 配套使用。當(dāng)使用 DisposableEffect 或者 LaunchedEffect時,代碼塊中用到某個值會在外部更新,如何獲取到最新的值呢?看一個例子,比如玩王者榮耀時,預(yù)選英雄,然后將英雄顯示出來,十秒倒計(jì)時后,顯示最終選擇的英雄,倒計(jì)時期間,可以改變選擇的英雄。
@Composable
fun ChooseHero() {
var sheshou by remember {
mutableStateOf("狄仁杰")
}
Column {
Text(text = "預(yù)選英雄: $sheshou")
Button(onClick = {
sheshou = "馬可波羅"
}) {
Text(text = "改選:馬可波羅")
}
FinalChoose(sheshou)
}
}
@Composable
fun FinalChoose(hero: String) {
var tips by remember {
mutableStateOf("游戲倒計(jì)時:10s")
}
LaunchedEffect(key1 = Unit) {
delay(10000)
tips = "最終選擇的英雄是:$hero"
}
Text(text = tips)
}
代碼運(yùn)行效果如下:

我們預(yù)選了狄仁杰,倒計(jì)時期間,點(diǎn)擊 button, 改選馬可波羅,最終選擇的英雄確顯示狄仁杰。
分析原因如下:在 FinalChoose 中參數(shù) hero 來源于外部,它的值改變,會觸發(fā)重組,但是,由于 LaunchedEffect 函數(shù),key 賦值 Unit, 重組過程中,協(xié)程代碼塊并不會重新執(zhí)行,感知不到外部的變化。要使能夠獲取到外部的最新值,一種方式是將 hero 作為 LaunchedEffect 的可觀察參數(shù)。修改代碼如下:
@Composable
fun FinalChoose(hero: String) {
var tips by remember {
mutableStateOf("游戲倒計(jì)時:10s")
}
LaunchedEffect(key1 = hero) {
delay(10000)
tips = "最終選擇的英雄是:$hero"
}
Text(text = tips)
}
此時再次執(zhí)行,在倒計(jì)時期間,我們點(diǎn)擊 button, 改變預(yù)選英雄,結(jié)果顯示正常了,最終選擇的即為馬可波羅。但是該方案并不符合我們的需求,前面講到, LaunchedEffect 的參數(shù) key,發(fā)生變化時,協(xié)程會取消,并重新啟動新的協(xié)程,這意味著,當(dāng)?shù)褂?jì)時過程中,我們改變了 key , 重新啟動的協(xié)程能夠獲取到改變后的值,但是倒計(jì)時也重新開始了,這顯然不是我們所期望的結(jié)果。
而 rememberUpdatedState 就是用來解決這種場景的。在不中斷協(xié)程的情況下,始終能夠獲取到最新的值??匆幌?rememberUpdatedState 如何使用。
我們把 LaunchedEffect 的參數(shù) key 還原成 Unit。使用 rememberUpdatedState 定義 currentHero。
@Composable
fun FinalChoose(hero: String) {
var tips by remember {
mutableStateOf("游戲倒計(jì)時:10s")
}
val currentHero by rememberUpdatedState(newValue = hero)
LaunchedEffect(key1 = Unit) {
delay(10000)
tips = "最終選擇的英雄是:$currentHero"
}
Text(text = tips)
}
這樣,運(yùn)行結(jié)果就符合我們的預(yù)期了。

2.6 derivedStateOf
上面的例子中,有一點(diǎn)不完美的地方,游戲倒計(jì)時時間沒有更新。下面使用 derivedStateOf 來優(yōu)化這個功能。
@Composable
fun FinalChoose(hero: String) {
var time by remember {
mutableIntStateOf(10)
}
val tips by remember {
derivedStateOf {
"游戲倒計(jì)時:${time}s"
}
}
LaunchedEffect(key1 = Unit) {
repeat(10) {
delay(1000)
time--
}
}
Text(
text = if (time == 0) {
"最終選擇的英雄是:$hero"
} else {
tips
}
)
}

現(xiàn)在效果好多了。這里我們不再需要 rememberUpdatedState 了。首先定義了時間,時一個 Int 類型的 State,然后借助 derivedStateOf 定義 tip ,時一個 String 類型的 State。
derivedStateOf 的作用是從一個或者多個 State 派生出另一個 State。如果某個狀態(tài)是從其他狀態(tài)對象計(jì)算或派生得出的,則可以使用 derivedStateOf。使用此函數(shù)可確保僅當(dāng)計(jì)算中使用的狀態(tài)之一發(fā)生變化時才會進(jìn)行計(jì)算。
derivedStateOf 的使用不難,但是和 remember 的配合使用可以有很多玩法來適應(yīng)不同的場景,主要的關(guān)注點(diǎn)還是在觸發(fā)重組的條件上,這個要綜合實(shí)際的場景和性能來覺得是用 key 來觸發(fā)重組還是改變引用的狀態(tài)來觸發(fā)重組。
2.7 snapshotFlow
前面使用 rememberUpdatedState 可以在 LaunchedEffect 中始終獲取到外部狀態(tài)的最新的值。但是無法感知到狀態(tài)的變化,也就是說外部狀態(tài)變化了,LaunchedEffect 中的代碼無法第一時間被通知到。用 snapshotFlow 則可以解決這個場景。
snapshotFlow 用于將一個 State<T> 轉(zhuǎn)換成一個協(xié)程中的 Flow。 當(dāng) snpashotFlow 塊中讀取到的 State 對象之一發(fā)生變化時,如果新值與之前發(fā)出的值不相等,F(xiàn)low 會向收集器發(fā)出最新的值(此行為類似于 Flow.distinctUntilChaned)。
看具體使用:
@Composable
fun FinalChoose(hero: String) {
var time by remember {
mutableIntStateOf(10)
}
var tips by remember {
mutableStateOf("游戲倒計(jì)時:10s")
}
LaunchedEffect(key1 = Unit) {
launch {
repeat(10) {
delay(1000)
time--
}
}
launch {
snapshotFlow { time }.collect {
tips = "游戲倒計(jì)時:${it}s"
}
}
}
Text(
text = if (time == 0) {
"最終選擇的英雄是:$hero"
} else {
tips
}
)
}
運(yùn)行結(jié)果和上一次一樣,這里我們不再使用 derivedStateOf, 而是啟動了兩個協(xié)程,一個協(xié)程用于倒計(jì)時技術(shù),另一個協(xié)程則將 time 這個 State 轉(zhuǎn)換成 Flow, 然后進(jìn)行收集,并更新 tips。
2.8 produceState
produceState 用于將任意外部數(shù)據(jù)源轉(zhuǎn)換為 State。
比如上面的例子中,我們將倒計(jì)時時間定義在 ViewModel 中,并且倒計(jì)時的邏輯在 ViewModel 中實(shí)現(xiàn),在 UI 中就可以借助 produceState 來實(shí)現(xiàn)。
@Composable
fun FinalChoose(hero: String) {
val time = viewModel.time
val tips by produceState<String>(initialValue = "游戲倒計(jì)時:10s") {
value = "游戲倒計(jì)時:${time}s"
awaitDispose {
// 做一些收尾的工作
}
}
Text(
text = if (time == 0) {
"最終選擇的英雄是:$hero"
} else {
tips
}
)
}
我們看一下 produceState 的源碼實(shí)現(xiàn):
@Composable
fun <T> produceState(
initialValue: T,
vararg keys: Any?,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
LaunchedEffect(keys = keys) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
很好理解,就是定義了一個狀態(tài) State, 然后啟動了一個協(xié)程,在協(xié)程中去更新 State 的值。參數(shù) key 發(fā)生變化時,協(xié)程會取消,然后重新啟動,生成新的 State。
同時注意到,在 produceState 中可以使用 awaitDispose{ } 方法做一些收尾工作。這是不是很容易聯(lián)想到 callbackFlow 的使用場景。沒錯,基于回調(diào)的接口實(shí)現(xiàn),利用 callbackFlow 很容易轉(zhuǎn)換為協(xié)程的 Flow, 而 produceState 即可將其轉(zhuǎn)換為 Compose 中的 State。比如 BroadcastReceiver、ContentProvider、網(wǎng)絡(luò)請求等等。
val currentPerson by produceState<Person?>(null, viewModel) {
val disposable = viewModel.registerPersonObserver { person ->
value = person
}
awaitDispose {
disposable.dispose()
}
}
再看一個網(wǎng)絡(luò)請求的例子:
@Composable
fun GetApi(url: String, repository: Repository): Recomposer.State<Result<Data>> {
return produceState(initialValue = Result.Loading, url, repository) {
val data = repository.load(url)
value = if (result == null) {
Result.Error
} else {
Result.Success(data)
}
}
}
三、總結(jié)
本文主要介紹了 Composable 的聲明周期,以及常用的副作用函數(shù)。
在重組過程中,應(yīng)該極力避免副作用的發(fā)生。根據(jù)場景,使用合適的副作用函數(shù)。
寫在最后
個人認(rèn)為 Compose 中最重要的知識域有兩個——狀態(tài)和重組、Modifier 修飾符。經(jīng)過前面這些文章的講解,狀態(tài)和重組基本上主要的知識點(diǎn)都講到了,知識有一定的前后連貫性。而 Modifier 修飾符龐大的類別體系中,將不再具有這樣的關(guān)聯(lián),可以挨個獨(dú)立學(xué)習(xí)。接下來的文章,我將不依次介紹 Modifier 的類別。而是介紹 Android 開發(fā)中的應(yīng)用領(lǐng)域在 Compose 中的處理方式,比如自定義 Layout, 動畫,觸摸反饋等等,然后在這些知識點(diǎn)中,講解涉及到的 Modifier。歡迎大家繼續(xù)關(guān)注!

浙公網(wǎng)安備 33010602011771號