Compose 中的副作用

連帶效果是指在 可組合函式的範圍 因可組合元件而造成生命週期和屬性,例如無法預測 重新組成、以不同順序重新組成可組合函式 可捨棄的重組,在理想情況下,可組合項應有連帶效果 免費

但有時還是必要的副作用,例如觸發一次性的 例如顯示 Snackbar 或根據 狀態條件。這些動作應從受控控制項呼叫 瞭解可組合元件生命週期的環境。在這個頁面中 您將瞭解 Jetpack Compose 提供的各種副作用 API。

狀態與效果的用途

Compose 中的思維說明文件所述,組件應完全避免副作用。需要 變更應用程式狀態 (如管理平台中的 狀態說明文件文件),建議您使用 API,以可預測的方式執行這些副作用

由於在 Compose 中開啟各種可能性的影響 過度使用確認其中執行的工作與 UI 相關 不會中斷單向資料流,詳情請參閱管理狀態 說明文件

LaunchedEffect:在可組合函式的範圍中執行暫停函式

可在可組合項的生命週期內執行工作,同時支援呼叫 暫停函式,請使用 LaunchedEffect 可組合函式。當 LaunchedEffect 進入「組成」中,會啟動協同程式並隨之傳遞程式碼區塊當做參數。如果 LaunchedEffect 離開組成,協同程式就會取消。如果 LaunchedEffect 是 使用不同的金鑰進行重組 (請參閱「重新啟動 Effects 一節),現有的協同程式將 新的暫停函式就會取消,並會在新的協同程式中啟動。

比方說,以下動畫會以 可設定的延遲時間:

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

在上述程式碼中,動畫使用了暫停函式 delay 等待設定的時間接著,系統會依序為 Alpha 值和 α 值 再按 animateTo. 這項作業會在可組合函式的生命週期內重複執行。

rememberCoroutineScope:取得組合感知範圍,在可組合函式外啟動協同程式

LaunchedEffect 是可組合函式,因此只能在其他可組合函式內部使用。如要在可組合項外部啟動協同程式 並指定範圍,讓它在離開 樂曲,使用 rememberCoroutineScope. 每當您需要控管rememberCoroutineScope 手動取消一或多個協同程式,例如,在 使用者事件

rememberCoroutineScope 是可以傳回 CoroutineScope 會繫結至呼叫的組成點。當呼叫離開組成時,系統就會取消此範圍。

沿續前述範例,您可以利用這組程式碼,在使用者輕觸 Button 時顯示 Snackbar

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        Column(Modifier.padding(contentPadding)) {
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

rememberUpdatedState:參照在值變更時不應重新啟動「效果」中的值

主要參數之一有所變更時,LaunchedEffect 會重新啟動。不過,在 有時候,您可能希望在影響力中捕捉任何值,如果可以 您不希望效果重新啟動。方法如下 必須使用 rememberUpdatedState 建立這個值的參照, 仍可擷取和更新如果效果包含 這類作業可能相當高昂或難以重新建立 重新啟動。

舉例來說,假設您的應用程式含有會在一段時間後消失的 LandingScreen。即使 LandingScreen 已重組,「作用」等待了一段時間,並通知經過的時間不應重新啟動:

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

為建立與呼叫站生命週期相符的「作用」,系統會傳遞永不改變的常數 (如 Unittrue) 做為參數。上述程式碼中使用了 LaunchedEffect(true)。為確保 onTimeout lambda「一律」含有重組 LandingScreen 時採用的最新值,必須使用 rememberUpdatedState 函式包覆 onTimeout。傳回的 State、程式碼中的 currentOnTimeout,都應運用在「作用」中。

DisposableEffect:需要清除的特效

需要在鍵變更後清除或 可組合函式會離開組成,請使用 DisposableEffect。 如果 DisposableEffect 金鑰有所變更,組件必須「處置」 (進行清除) 目前的「作用」,並再次呼叫「作用」進行重設。

舉例來說,您可能會想根據資料傳送 Analytics 事件 Lifecycle 事件 方法是使用 LifecycleObserver. 如要在 Compose 中監聽這些事件,請視需要使用 DisposableEffect 註冊和取消註冊觀察工具。

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

在上述程式碼中,「作用」會將 observer 新增至 lifecycleOwner。如果 lifecycleOwner 有所變更,系統就會棄置「作用」再以新的 lifecycleOwner 將其重新啟動。

DisposableEffect 必須納入 onDispose 子句做為最終陳述式 程式碼區塊中的各個項目否則 IDE 會顯示建構時間錯誤。

SideEffect:將 Compose 狀態發布至非 Compose 程式碼

如要與不受 Compose 管理的物件共用 Compose 狀態,請使用 SideEffect 可組合函式。使用 SideEffect 可保證生效會在您每次 才能順利重組另一方面,這個字則不正確 才會成功執行重組程序 出現這種狀況。

舉例來說,數據分析資料庫可能讓您區隔使用者 來連結自訂中繼資料 (在此範例中為「使用者屬性」) 套用至所有後續 Analytics 事件如要將目前使用者的使用者類型連接到數據分析程式庫,請使用 SideEffect 更新其值。

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState:將非 Compose 狀態轉換為 Compose 狀態

produceState 會啟動一個限定範圍為組合的協同程式,該協同程式可以將值推送至 傳回 State。用途 將非 Compose 狀態轉換為 Compose 狀態,例如 訂閱導向狀態 (例如 FlowLiveDataRxJava) 樂曲。

produceState 進入「組成」中,制作工具就會啟動;而離開「組成」時,製作工具就會取消。傳回的 State 會混合起來,設定相同的值不會觸發重組。

雖然 produceState 會建立協同程式,但也可以用來觀察非暫停的資料來源。如要移除針對該來源的訂閱,請使用 awaitDispose 函式。

以下範例說明如何使用 produceState 從網路載入圖片。loadNetworkImage 可組合函式會傳回可用於其他組件的 State

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

derivedStateOf:將一或多個狀態物件轉換成其他狀態

在 Compose 中,發生「重組」的情形 每次觀察到的狀態物件或可組合函式輸入內容變更時。狀態物件 比起 UI 實際需要更新,輸入內容可能會有變動 以免發生不必要的重組

您應使用 derivedStateOf 當可組合項的輸入內容變動頻率超出所需時,就會發生此函式 才能重新撰寫這通常是因為某些項目經常變更 (例如 捲動位置,但可組合函式僅需跨越捲動位置時回應 達到特定門檻derivedStateOf 會建立新的 Compose 狀態物件 可以發現只視需要更新這樣一來 類似於 Kotlin 資料流 distinctUntilChanged() 運算子。

正確使用方式

下列程式碼片段為 derivedStateOf 的適當用途:

@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

在這段程式碼中,每當第一個顯示項目時,firstVisibleItemIndex 都會變更 並輸入變更內容捲動時,該值會變為 012345 等。 不過,只有在值大於 0 時,才需要進行重組。 而這種更新頻率不一致,表示 derivedStateOf

用法不正確

常見的錯誤是,當您合併兩個 Compose 狀態物件時 您應使用 derivedStateOf,因為您正在「衍生狀態」。不過, 您只需要負擔大量額外費用,如下列程式碼片段所示:

// DO NOT USE. Incorrect usage of derivedStateOf.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct

在這個程式碼片段中,fullName 需要像 firstNamefirstName 一樣更新 lastName。因此不會出現過度的重組程序 不需要 derivedStateOf

snapshotFlow:將 Compose 的狀態轉換為資料流

使用 snapshotFlowState<T> 物件轉換至冷流程。snapshotFlow 會在收集後執行其區塊,並發出在其中讀取到的 State 物件結果。出現其中一個 State 物件時 snapshotFlow 區塊內的讀取結果改變,資料流就會發出新的值 其值「不等於」 (這個行為與 Flow.distinctUntilChanged)。

以下範例顯示使用者捲動經過清單中第一個項目前往數據分析時會進行記錄的副作用:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

在上述程式碼中,listState.firstVisibleItemIndex 會轉換成能從流程運算子的強大功能受益的流程。

重新啟動「作用」

Compose 中的部分特效,例如 LaunchedEffectproduceStateDisposableEffect ) 採用變數數量的引數、鍵,以便用於 取消執行中的效果,然後使用新的按鍵開始新的效果。

這些 API 的一般形式如下:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

由於這類行為有細微差異, 但用於重新啟動「作用」不是正確的:

  • 重新啟動的「作用」少於應有的數量,可能造成應用程式中發生錯誤。
  • 重新啟動的「作用」多於應有的數量,可能使效率低落。

原則上,在程式碼的「作用」區塊使用的可變動和不可變動變數,應該新增為「作用」組件的參數。除此之外,您可以新增更多參數,以強制重新啟動「作用」。如果變更 變數應該不會導致「效果」重新啟動,變數必須經過包裝 在 rememberUpdatedState 中。如果變數從未存在 但由於此函式位於不含金鑰的 remember 中,因此您不需要 將變數當做金鑰傳遞至「作用」。

在上述的 DisposableEffect 程式碼中,效果會以參數形式呈現 區塊中所用的 lifecycleOwner,因為任何變更都會產生

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // These values never change in Composition
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            /* ... */
        }

        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

DisposableEffect 不需要 currentOnStartcurrentOnStop 金鑰的值,因為使用 rememberUpdatedState。如果您沒有將 lifecycleOwner 做為參數傳遞, 內容有變,HomeScreen 也會重新組成,但 DisposableEffect 並未處理 並重新啟動由於 lifecycleOwner 不是 不再使用

以常值為金鑰

您可以使用 true 這類常值做為「作用」金鑰,使其遵循呼叫站的生命週期。有效的用途確實存在,例如前述的 LaunchedEffect 範例。不過,在實際執行之前,請多考慮一下,確認您真的需要這麼做。