- 一、 Composable 的生命周期
-
二、 Composable 的副作用
- 2.1 SideEffect
- 2.2 DisposableEffect
- 2.3 LaunchedEffect
- 2.4 rememberCoroutineScope
- 2.5 rememberUpdatedState
- 2.6 derivedStateOf
- 2.7 snapshotFlow
- 2.8 produceState
- 三、總結(jié)
- 寫在最后
一、 Composable 的生命周期
Composable 組件都是函數(shù),Composable 函數(shù)執(zhí)行會(huì)得到一棵視圖樹,每一個(gè) Composable 組件對(duì)應(yīng)視圖樹上的一個(gè)節(jié)點(diǎn)。Composable 的生命周期定義如下:
- onActive(添加到視圖樹) Composable 首次被執(zhí)行,即在視圖樹上創(chuàng)建對(duì)應(yīng)的節(jié)點(diǎn)。
- onUpdate(重組) Composable 跟隨重組不斷執(zhí)行,更新視圖樹上對(duì)應(yīng)的節(jié)點(diǎn)。
- onDispose(從視圖樹移除) Composable 不再被執(zhí)行,對(duì)應(yīng)節(jié)點(diǎn)從視圖樹上移除。
對(duì)于 Compose 編寫 UI 來說,頁面的變化,是依靠狀態(tài)的變化,Composable 進(jìn)行重組,渲染出不同的頁面。當(dāng)頁面可見時(shí),對(duì)應(yīng)的節(jié)點(diǎn)被添加到視圖樹,當(dāng)頁面不可見時(shí),對(duì)應(yīng)的節(jié)點(diǎn)從視圖樹移除。所以,雖然 Activity 有前后臺(tái)的概念,但是使用 Compose 編寫的頁面,對(duì)于 Composable 沒有前后臺(tái)切換的概念。當(dāng)頁面切換為不可見時(shí),對(duì)應(yīng)的節(jié)點(diǎn)也被立即銷毀了,不會(huì)像 Activity 或者 Fragment 那樣在后臺(tái)保存實(shí)例。
二、 Composable 的副作用
上一篇將重組的文章講到,Composable 重組過程中可能反復(fù)執(zhí)行,并且中間環(huán)節(jié)有可能被打斷,只保證最后一次執(zhí)行的狀態(tài)時(shí)正確的。
試想一個(gè)問題,如果在 Composable 函數(shù)中彈一個(gè) Toast ,當(dāng) Composable 發(fā)生重組時(shí),這個(gè) Toast 會(huì)彈多少次,是不是就無法控制了。再比如,在 Composable 函數(shù)中讀寫函數(shù)之外的變量,讀寫文件,請(qǐng)求網(wǎng)絡(luò)等等,這些操作是不是都無法得到保證了。類似這樣,在 Composable 執(zhí)行過程中,凡是會(huì)影響外界的操作,都屬于副作用。在 Composable 重組過程中,這些副作用行為都難以得到保證,那怎么辦?為了是副作用只發(fā)生在生命周期的特定階段, Compose 提供了一系列副作用函數(shù),來確保行為的可預(yù)期性。下面,我們看看這些副作用函數(shù)的使用場景。
2.1 SideEffect
SideEffect 在每次成功重組的時(shí)候都會(huì)執(zhí)行。
Composable 在重組過程中會(huì)反復(fù)執(zhí)行,但是重組不一定每次都會(huì)成功,有的可能會(huì)被中斷,中途失敗。 SideEffect 僅在重組成功的時(shí)候才會(huì)執(zhí)行。
特點(diǎn):
- 重組成功才會(huì)執(zhí)行。
- 有可能會(huì)執(zhí)行多次。
所以,SideEffect 函數(shù)不能用來執(zhí)行耗時(shí)操作,或者只要求執(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ù)處理和收尾工作。
典型的使用的場景,注冊(cè)與取消注冊(cè):
DisposableEffect(vararg keys: Any?) {
// register(callback)
onDispose {
// unregister(callback)
}
}
這里首先參數(shù) keys 表示,當(dāng) keys 變化時(shí), DisposableEffect 會(huì)重新執(zhí)行,如果在整個(gè)生命周期內(nèi),只想執(zhí)行一次,則可以傳入 Unit
而 onDispose
代碼塊則會(huì)在 Composable 進(jìn)入 onDispose 時(shí)執(zhí)行。
2.3 LaunchedEffect
LaunchedEffect 用于在 Composable 中啟動(dòng)協(xié)程,當(dāng) Composable 進(jìn)入 onAtive 時(shí),LaunchedEffect 會(huì)自動(dòng)啟動(dòng)協(xié)程,執(zhí)行 block 中的代碼。當(dāng) Composable 進(jìn)入 onDispose 時(shí),協(xié)程會(huì)自動(dòng)取消。
使用方法:
LaunchedEffect(vararg keys: Any?) {
// do Something async
}
同樣支持可觀察參數(shù),當(dāng) key 變化時(shí),當(dāng)前協(xié)程自動(dòng)結(jié)束,同時(shí)開啟新協(xié)程。
2.4 rememberCoroutineScope
LaunchedEffect 只能在 Composable 中調(diào)用,如果想在非 Composable 環(huán)境中使用協(xié)程,比如在 Button 的 OnClick 中開啟協(xié)程,并希望在 Composable 進(jìn)入 onDispose 時(shí)自動(dòng)取消,則可以使用 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
時(shí),代碼塊中用到某個(gè)值會(huì)在外部更新,如何獲取到最新的值呢?看一個(gè)例子,比如玩王者榮耀時(shí),預(yù)選英雄,然后將英雄顯示出來,十秒倒計(jì)時(shí)后,顯示最終選擇的英雄,倒計(jì)時(shí)期間,可以改變選擇的英雄。
@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ì)時(shí):10s")
}
LaunchedEffect(key1 = Unit) {
delay(10000)
tips = "最終選擇的英雄是:$hero"
}
Text(text = tips)
}
代碼運(yùn)行效果如下:
我們預(yù)選了狄仁杰,倒計(jì)時(shí)期間,點(diǎn)擊 button, 改選馬可波羅,最終選擇的英雄確顯示狄仁杰。
分析原因如下:在 FinalChoose
中參數(shù) hero
來源于外部,它的值改變,會(huì)觸發(fā)重組,但是,由于 LaunchedEffect 函數(shù),key 賦值 Unit
, 重組過程中,協(xié)程代碼塊并不會(huì)重新執(zhí)行,感知不到外部的變化。要使能夠獲取到外部的最新值,一種方式是將 hero
作為 LaunchedEffect 的可觀察參數(shù)。修改代碼如下:
@Composable
fun FinalChoose(hero: String) {
var tips by remember {
mutableStateOf("游戲倒計(jì)時(shí):10s")
}
LaunchedEffect(key1 = hero) {
delay(10000)
tips = "最終選擇的英雄是:$hero"
}
Text(text = tips)
}
此時(shí)再次執(zhí)行,在倒計(jì)時(shí)期間,我們點(diǎn)擊 button, 改變預(yù)選英雄,結(jié)果顯示正常了,最終選擇的即為馬可波羅。但是該方案并不符合我們的需求,前面講到, LaunchedEffect 的參數(shù) key,發(fā)生變化時(shí),協(xié)程會(huì)取消,并重新啟動(dòng)新的協(xié)程,這意味著,當(dāng)?shù)褂?jì)時(shí)過程中,我們改變了 key , 重新啟動(dòng)的協(xié)程能夠獲取到改變后的值,但是倒計(jì)時(shí)也重新開始了,這顯然不是我們所期望的結(jié)果。
而 rememberUpdatedState
就是用來解決這種場景的。在不中斷協(xié)程的情況下,始終能夠獲取到最新的值??匆幌?rememberUpdatedState 如何使用。
我們把 LaunchedEffect 的參數(shù) key 還原成 Unit。使用 rememberUpdatedState 定義 currentHero。
@Composable
fun FinalChoose(hero: String) {
var tips by remember {
mutableStateOf("游戲倒計(jì)時(shí):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ì)時(shí)時(shí)間沒有更新。下面使用 derivedStateOf
來優(yōu)化這個(gè)功能。
@Composable
fun FinalChoose(hero: String) {
var time by remember {
mutableIntStateOf(10)
}
val tips by remember {
derivedStateOf {
"游戲倒計(jì)時(shí):${time}s"
}
}
LaunchedEffect(key1 = Unit) {
repeat(10) {
delay(1000)
time--
}
}
Text(
text = if (time == 0) {
"最終選擇的英雄是:$hero"
} else {
tips
}
)
}
現(xiàn)在效果好多了。這里我們不再需要 rememberUpdatedState 了。首先定義了時(shí)間,時(shí)一個(gè) Int 類型的 State,然后借助 derivedStateOf 定義 tip ,時(shí)一個(gè) String 類型的 State。
derivedStateOf 的作用是從一個(gè)或者多個(gè) State 派生出另一個(gè) State。如果某個(gè)狀態(tài)是從其他狀態(tài)對(duì)象計(jì)算或派生得出的,則可以使用 derivedStateOf
。使用此函數(shù)可確保僅當(dāng)計(jì)算中使用的狀態(tài)之一發(fā)生變化時(shí)才會(huì)進(jìn)行計(jì)算。
derivedStateOf 的使用不難,但是和 remember 的配合使用可以有很多玩法來適應(yīng)不同的場景,主要的關(guān)注點(diǎn)還是在觸發(fā)重組的條件上,這個(gè)要綜合實(shí)際的場景和性能來覺得是用 key 來觸發(fā)重組還是改變引用的狀態(tài)來觸發(fā)重組。
2.7 snapshotFlow
前面使用 rememberUpdatedState 可以在 LaunchedEffect 中始終獲取到外部狀態(tài)的最新的值。但是無法感知到狀態(tài)的變化,也就是說外部狀態(tài)變化了,LaunchedEffect 中的代碼無法第一時(shí)間被通知到。用 snapshotFlow 則可以解決這個(gè)場景。
snapshotFlow 用于將一個(gè) State<T>
轉(zhuǎn)換成一個(gè)協(xié)程中的 Flow。 當(dāng) snpashotFlow 塊中讀取到的 State 對(duì)象之一發(fā)生變化時(shí),如果新值與之前發(fā)出的值不相等,F(xiàn)low 會(huì)向收集器發(fā)出最新的值(此行為類似于 Flow.distinctUntilChaned)。
看具體使用:
@Composable
fun FinalChoose(hero: String) {
var time by remember {
mutableIntStateOf(10)
}
var tips by remember {
mutableStateOf("游戲倒計(jì)時(shí):10s")
}
LaunchedEffect(key1 = Unit) {
launch {
repeat(10) {
delay(1000)
time--
}
}
launch {
snapshotFlow { time }.collect {
tips = "游戲倒計(jì)時(shí):${it}s"
}
}
}
Text(
text = if (time == 0) {
"最終選擇的英雄是:$hero"
} else {
tips
}
)
}
運(yùn)行結(jié)果和上一次一樣,這里我們不再使用 derivedStateOf
, 而是啟動(dòng)了兩個(gè)協(xié)程,一個(gè)協(xié)程用于倒計(jì)時(shí)技術(shù),另一個(gè)協(xié)程則將 time 這個(gè) State 轉(zhuǎn)換成 Flow, 然后進(jìn)行收集,并更新 tips。
2.8 produceState
produceState
用于將任意外部數(shù)據(jù)源轉(zhuǎn)換為 State。
比如上面的例子中,我們將倒計(jì)時(shí)時(shí)間定義在 ViewModel 中,并且倒計(jì)時(shí)的邏輯在 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ì)時(shí):10s") {
value = "游戲倒計(jì)時(shí):${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
}
很好理解,就是定義了一個(gè)狀態(tài) State, 然后啟動(dòng)了一個(gè)協(xié)程,在協(xié)程中去更新 State 的值。參數(shù) key 發(fā)生變化時(shí),協(xié)程會(huì)取消,然后重新啟動(dòng),生成新的 State。
同時(shí)注意到,在 produceState 中可以使用 awaitDispose{ }
方法做一些收尾工作。這是不是很容易聯(lián)想到 callbackFlow
的使用場景。沒錯(cuò),基于回調(diào)的接口實(shí)現(xiàn),利用 callbackFlow
很容易轉(zhuǎn)換為協(xié)程的 Flow, 而 produceState
即可將其轉(zhuǎn)換為 Compose 中的 State。比如 BroadcastReceiver、ContentProvider、網(wǎng)絡(luò)請(qǐng)求等等。
val currentPerson by produceState<Person?>(null, viewModel) {
val disposable = viewModel.registerPersonObserver { person ->
value = person
}
awaitDispose {
disposable.dispose()
}
}
再看一個(gè)網(wǎng)絡(luò)請(qǐng)求的例子:
@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ù)。文章來源:http://www.zghlxwxcb.cn/news/detail-844285.html
寫在最后
個(gè)人認(rèn)為 Compose 中最重要的知識(shí)域有兩個(gè)——狀態(tài)和重組、Modifier 修飾符。經(jīng)過前面這些文章的講解,狀態(tài)和重組基本上主要的知識(shí)點(diǎn)都講到了,知識(shí)有一定的前后連貫性。而 Modifier 修飾符龐大的類別體系中,將不再具有這樣的關(guān)聯(lián),可以挨個(gè)獨(dú)立學(xué)習(xí)。接下來的文章,我將不依次介紹 Modifier 的類別。而是介紹 Android 開發(fā)中的應(yīng)用領(lǐng)域在 Compose 中的處理方式,比如自定義 Layout, 動(dòng)畫,觸摸反饋等等,然后在這些知識(shí)點(diǎn)中,講解涉及到的 Modifier。歡迎大家繼續(xù)關(guān)注!文章來源地址http://www.zghlxwxcb.cn/news/detail-844285.html
到了這里,關(guān)于Jetpack Compose(5)——生命周期與副作用函數(shù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!