Jetpack Compose(5)——生命週期與副作用函數

一、 Composable 的生命週期

Composable 組件都是函數,Composable 函數執行會得到一棵視圖樹,每一個 Composable 組件對應視圖樹上的一個節點。Composable 的生命週期定義如下:

  • onActive(添加到視圖樹) Composable 首次被執行,即在視圖樹上創建對應的節點。
  • onUpdate(重組) Composable 跟隨重組不斷執行,更新視圖樹上對應的節點。
  • onDispose(從視圖樹移除) Composable 不再被執行,對應節點從視圖樹上移除。

對於 Compose 編寫 UI 來說,頁面的變化,是依靠狀態的變化,Composable 進行重組,渲染出不同的頁面。當頁面可見時,對應的節點被添加到視圖樹,當頁面不可見時,對應的節點從視圖樹移除。所以,雖然 Activity 有前後臺的概念,但是使用 Compose 編寫的頁面,對於 Composable 沒有前後臺切換的概念。當頁面切換爲不可見時,對應的節點也被立即銷燬了,不會像 Activity 或者 Fragment 那樣在後臺保存實例。

二、 Composable 的副作用

上一篇將重組的文章講到,Composable 重組過程中可能反覆執行,並且中間環節有可能被打斷,只保證最後一次執行的狀態時正確的。
試想一個問題,如果在 Composable 函數中彈一個 Toast ,當 Composable 發生重組時,這個 Toast 會彈多少次,是不是就無法控制了。再比如,在 Composable 函數中讀寫函數之外的變量,讀寫文件,請求網絡等等,這些操作是不是都無法得到保證了。類似這樣,在 Composable 執行過程中,凡是會影響外界的操作,都屬於副作用。在 Composable 重組過程中,這些副作用行爲都難以得到保證,那怎麼辦?爲了是副作用只發生在生命週期的特定階段, Compose 提供了一系列副作用函數,來確保行爲的可預期性。下面,我們看看這些副作用函數的使用場景。

2.1 SideEffect

SideEffect 在每次成功重組的時候都會執行。
Composable 在重組過程中會反覆執行,但是重組不一定每次都會成功,有的可能會被中斷,中途失敗。 SideEffect 僅在重組成功的時候纔會執行

特點:

  1. 重組成功纔會執行。
  2. 有可能會執行多次。
    所以,SideEffect 函數不能用來執行耗時操作,或者只要求執行一次的操作。

典型使用場景,比如在主題中設置狀態欄,導航欄顏色等。

SideEffect {
    val window = (view.context as Activity).window
    window.statusBarColor = colorScheme.primary.toArgb()
    WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}

2.2 DisposableEffect

DisposableEffect 可以感知 Composable 的 onActiveonDispose, 允許使用該函數完成一些預處理和收尾工作。

典型的使用的場景,註冊與取消註冊:

DisposableEffect(vararg keys: Any?) {
    // register(callback)
    onDispose {
        // unregister(callback)
    }
}

這裏首先參數 keys 表示,當 keys 變化時, DisposableEffect 會重新執行,如果在整個生命週期內,只想執行一次,則可以傳入 Unit
onDispose 代碼塊則會在 Composable 進入 onDispose 時執行。

2.3 LaunchedEffect

LaunchedEffect 用於在 Composable 中啓動協程,當 Composable 進入 onAtive 時,LaunchedEffect 會自動啓動協程,執行 block 中的代碼。當 Composable 進入 onDispose 時,協程會自動取消。
使用方法:

LaunchedEffect(vararg keys: Any?) {
    // do Something async
}

同樣支持可觀察參數,當 key 變化時,當前協程自動結束,同時開啓新協程。

2.4 rememberCoroutineScope

LaunchedEffect 只能在 Composable 中調用,如果想在非 Composable 環境中使用協程,比如在 Button 的 OnClick 中開啓協程,並希望在 Composable 進入 onDispose 時自動取消,則可以使用 rememberCoroutineScope 。
具體用法如下:

@Composable
fun Test() {
    val scope = rememberCoroutineScope()
    Button(
        onClick = {
            scope.launch {
                // do something
            }
        }
    ) {
        Text("click me")
    }
}

DisposableEffect 配合 rememberCoroutineScope 可以實現 LaunchedEffect 同樣的效果,但是一般這樣做沒有什麼意義。

2.5 rememberUpdatedState

rememberUpdatedState 一般和 DisposableEffect 或者 LaunchedEffect 配套使用。當使用 DisposableEffect 或者 LaunchedEffect時,代碼塊中用到某個值會在外部更新,如何獲取到最新的值呢?看一個例子,比如玩王者榮耀時,預選英雄,然後將英雄顯示出來,十秒倒計時後,顯示最終選擇的英雄,倒計時期間,可以改變選擇的英雄。

@Composable
fun ChooseHero() {
    var sheshou by remember {
        mutableStateOf("狄仁傑")
    }

    Column {
        Text(text = "預選英雄: $sheshou")
        Button(onClick = {
            sheshou = "馬可波羅"
        }) {
            Text(text = "改選:馬可波羅")
        }
        FinalChoose(sheshou)
    }
}

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("遊戲倒計時:10s")
    }
    LaunchedEffect(key1 = Unit) {
        delay(10000)
        tips = "最終選擇的英雄是:$hero"
    }
    Text(text = tips)
}

代碼運行效果如下:

我們預選了狄仁傑,倒計時期間,點擊 button, 改選馬可波羅,最終選擇的英雄確顯示狄仁傑。
分析原因如下:在 FinalChoose 中參數 hero 來源於外部,它的值改變,會觸發重組,但是,由於 LaunchedEffect 函數,key 賦值 Unit, 重組過程中,協程代碼塊並不會重新執行,感知不到外部的變化。要使能夠獲取到外部的最新值,一種方式是將 hero 作爲 LaunchedEffect 的可觀察參數。修改代碼如下:

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("遊戲倒計時:10s")
    }
    LaunchedEffect(key1 = hero) {
        delay(10000)
        tips = "最終選擇的英雄是:$hero"
    }
    Text(text = tips)
}

此時再次執行,在倒計時期間,我們點擊 button, 改變預選英雄,結果顯示正常了,最終選擇的即爲馬可波羅。但是該方案並不符合我們的需求,前面講到, LaunchedEffect 的參數 key,發生變化時,協程會取消,並重新啓動新的協程,這意味着,當倒計時過程中,我們改變了 key , 重新啓動的協程能夠獲取到改變後的值,但是倒計時也重新開始了,這顯然不是我們所期望的結果。

rememberUpdatedState 就是用來解決這種場景的。在不中斷協程的情況下,始終能夠獲取到最新的值。看一下 rememberUpdatedState 如何使用。
我們把 LaunchedEffect 的參數 key 還原成 Unit。使用 rememberUpdatedState 定義 currentHero。

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("遊戲倒計時:10s")
    }

    val currentHero by rememberUpdatedState(newValue = hero)

    LaunchedEffect(key1 = Unit) {
        delay(10000)
        tips = "最終選擇的英雄是:$currentHero"
    }
    Text(text = tips)
}

這樣,運行結果就符合我們的預期了。

2.6 derivedStateOf

上面的例子中,有一點不完美的地方,遊戲倒計時時間沒有更新。下面使用 derivedStateOf 來優化這個功能。

@Composable
fun FinalChoose(hero: String) {
    var time by remember {
        mutableIntStateOf(10)
    }

    val tips by remember {
        derivedStateOf {
            "遊戲倒計時:${time}s"
        }
    }

    LaunchedEffect(key1 = Unit) {
        repeat(10) {
            delay(1000)
            time--
        }
    }
    Text(
        text = if (time == 0) {
            "最終選擇的英雄是:$hero"
        } else {
            tips
        }
    )
}

現在效果好多了。這裏我們不再需要 rememberUpdatedState 了。首先定義了時間,時一個 Int 類型的 State,然後藉助 derivedStateOf 定義 tip ,時一個 String 類型的 State。
derivedStateOf 的作用是從一個或者多個 State 派生出另一個 State。如果某個狀態是從其他狀態對象計算或派生得出的,則可以使用 derivedStateOf。使用此函數可確保僅當計算中使用的狀態之一發生變化時纔會進行計算。
derivedStateOf 的使用不難,但是和 remember 的配合使用可以有很多玩法來適應不同的場景,主要的關注點還是在觸發重組的條件上,這個要綜合實際的場景和性能來覺得是用 key 來觸發重組還是改變引用的狀態來觸發重組。

2.7 snapshotFlow

前面使用 rememberUpdatedState 可以在 LaunchedEffect 中始終獲取到外部狀態的最新的值。但是無法感知到狀態的變化,也就是說外部狀態變化了,LaunchedEffect 中的代碼無法第一時間被通知到。用 snapshotFlow 則可以解決這個場景。
snapshotFlow 用於將一個 State<T> 轉換成一個協程中的 Flow。 當 snpashotFlow 塊中讀取到的 State 對象之一發生變化時,如果新值與之前發出的值不相等,Flow 會向收集器發出最新的值(此行爲類似於 Flow.distinctUntilChaned)。
看具體使用:

@Composable
fun FinalChoose(hero: String) {
    var time by remember {
        mutableIntStateOf(10)
    }

    var tips by remember {
        mutableStateOf("遊戲倒計時:10s")
    }

    LaunchedEffect(key1 = Unit) {
        launch {
            repeat(10) {
                delay(1000)
                time--
            }
        }
        launch {
            snapshotFlow { time }.collect {
                    tips = "遊戲倒計時:${it}s"
                }
        }
    }

    Text(
        text = if (time == 0) {
            "最終選擇的英雄是:$hero"
        } else {
            tips
        }
    )
}

運行結果和上一次一樣,這裏我們不再使用 derivedStateOf, 而是啓動了兩個協程,一個協程用於倒計時技術,另一個協程則將 time 這個 State 轉換成 Flow, 然後進行收集,並更新 tips。

2.8 produceState

produceState 用於將任意外部數據源轉換爲 State。
比如上面的例子中,我們將倒計時時間定義在 ViewModel 中,並且倒計時的邏輯在 ViewModel 中實現,在 UI 中就可以藉助 produceState 來實現。

@Composable
fun FinalChoose(hero: String) {
    val time = viewModel.time

    val tips by produceState<String>(initialValue = "遊戲倒計時:10s") {
        value = "遊戲倒計時:${time}s"

        awaitDispose {
            // 做一些收尾的工作
        }
    }
    Text(
        text = if (time == 0) {
            "最終選擇的英雄是:$hero"
        } else {
            tips
        }
    )
}

我們看一下 produceState 的源碼實現:

@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
}

很好理解,就是定義了一個狀態 State, 然後啓動了一個協程,在協程中去更新 State 的值。參數 key 發生變化時,協程會取消,然後重新啓動,生成新的 State。
同時注意到,在 produceState 中可以使用 awaitDispose{ } 方法做一些收尾工作。這是不是很容易聯想到 callbackFlow 的使用場景。沒錯,基於回調的接口實現,利用 callbackFlow 很容易轉換爲協程的 Flow, 而 produceState 即可將其轉換爲 Compose 中的 State。比如 BroadcastReceiver、ContentProvider、網絡請求等等。

val currentPerson by produceState<Person?>(null, viewModel) {
    val disposable = viewModel.registerPersonObserver { person ->
        value = person
    }

    awaitDispose {
        disposable.dispose()
    }
}

再看一個網絡請求的例子:

@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)
        }
    }
}

三、總結

本文主要介紹了 Composable 的聲明週期,以及常用的副作用函數。
在重組過程中,應該極力避免副作用的發生。根據場景,使用合適的副作用函數。

寫在最後

個人認爲 Compose 中最重要的知識域有兩個——狀態和重組、Modifier 修飾符。經過前面這些文章的講解,狀態和重組基本上主要的知識點都講到了,知識有一定的前後連貫性。而 Modifier 修飾符龐大的類別體系中,將不再具有這樣的關聯,可以挨個獨立學習。接下來的文章,我將不依次介紹 Modifier 的類別。而是介紹 Android 開發中的應用領域在 Compose 中的處理方式,比如自定義 Layout, 動畫,觸摸反饋等等,然後在這些知識點中,講解涉及到的 Modifier。歡迎大家繼續關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章