連帶效果是指在可組合函式範圍外發生的應用程式狀態變更。由於可組合項的生命週期和屬性 (例如無法預測的重組)、以不同順序執行可組合項的重組,或是可捨棄的重組,可組合項理想情況下應無副作用。
但有時還是需要副作用,例如為了觸發一次性事件,例如顯示 Snackbar,或根據特定狀態條件前往其他畫面。您應從瞭解可組合項生命週期的受控環境呼叫這些動作。在這個頁面中,您將瞭解 Jetpack Compose 提供的各種副作用 API。
狀態與效果的用途
如 Compose 中的思維說明文件所述,組件應完全避免副作用。如果需要變更應用程式的狀態 (如「管理狀態說明文件」文件中所述),應使用 Effect API,以可預測的方式執行這些副作用。
在 Compose 中開啟的不同可能性影響,因此很容易遭到過度使用。請確認您在其中執行的工作與 UI 相關,且不會中斷單向資料流,詳情請參閱管理狀態說明文件。
LaunchedEffect
:在可組合函式的範圍中執行暫停函式
如要從組件內部安全地呼叫暫停函式,請使用 LaunchedEffect
組件。當 LaunchedEffect
進入「組成」中,會啟動協同程式並隨之傳遞程式碼區塊當做參數。如果 LaunchedEffect
離開組成,協同程式就會取消。如果使用不同金鑰重組 LaunchedEffect
(請參閱下方的「重新啟動效果」一節),系統會取消現有的協同程式,並在新的協同程式中啟動新的暫停函式。
舉例來說,在 Scaffold
中顯示 Snackbar
要透過 SnackbarHostState.showSnackbar
函式執行,這是一個暫停函式。
@Composable fun MyScreen( state: UiState<List<Movie>>, snackbarHostState: SnackbarHostState ) { // If the UI state contains an error, show snackbar if (state.hasError) { // `LaunchedEffect` will cancel and re-launch if // `scaffoldState.snackbarHostState` changes LaunchedEffect(snackbarHostState) { // Show snackbar using a coroutine, when the coroutine is cancelled the // snackbar will automatically dismiss. This coroutine will cancel whenever // `state.hasError` is false, and only start when `state.hasError` is true // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes. snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> // ... } }
在上述程式碼中,如果該狀態含有錯誤就會觸發協同程式,如果不含錯誤就會取消。由於 LaunchedEffect
呼叫網站位於 if 陳述式內,當陳述式為 false 時,如果 LaunchedEffect
出現在組合中,系統會移除該陳述式,並取消協同程式。
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 */ }
為建立與呼叫站生命週期相符的「作用」,系統會傳遞永不改變的常數 (如 Unit
或 true
) 做為參數。上述程式碼中使用了 LaunchedEffect(true)
。為確保 onTimeout
lambda「一律」含有重組 LandingScreen
時採用的最新值,必須使用 rememberUpdatedState
函式包覆 onTimeout
。傳回的 State
、程式碼中的 currentOnTimeout
,都應運用在「作用」中。
DisposableEffect
:需要清除的特效
對於鍵變更後或可組合項離開組合時,需要「清理」的連帶效果,請使用 DisposableEffect
。如果 DisposableEffect
金鑰有所變更,組件必須「處置」 (進行清除) 目前的「作用」,並再次呼叫「作用」進行重設。
舉例來說,您可能想要使用 LifecycleObserver
,根據 Lifecycle
事件傳送數據分析事件。如要在 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
可保證效果會在每次成功重組後執行。另一方面,在系統保證成功重組前執行效果並不容易,也就是在可組合項中直接寫入效果時的情況。
舉例來說,數據分析程式庫可能會允許在所有後續數據分析事件中附加自訂中繼資料 (在此例中為「使用者屬性」),來區隔使用者人口。如要將目前使用者的使用者類型連接到數據分析程式庫,請使用 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 狀態,例如將外部以訂閱為準的狀態 (例如 Flow
、LiveData
或 RxJava
) 帶入 Composition (組合)。
當 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
都會變更。捲動時,該值會變為 0
、1
、2
、3
、4
、5
等。不過,只有在值大於 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
需要像 firstName
和 lastName
一樣更新。因此,不會發生額外的重組情形,而且不需要使用 derivedStateOf
。
snapshotFlow
:將 Compose 的狀態轉換為資料流
使用 snapshotFlow
將 State<T>
物件轉換至冷流程。snapshotFlow
會在收集後執行其區塊,並發出在其中讀取到的 State
物件結果。當 snapshotFlow
區塊中讀取的其中一個 State
物件有變時,如果新值「不等於」先前所發出的值,Flow 就會向收集器發出新值 (這個行為與 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 中的部分「作用」(例如 LaunchedEffect
、produceState
或 DisposableEffect
) 會採用數量不定的引數、金鑰,可用於取消執行中的「作用」,並使用新金鑰啟動新的「作用」。
這些 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
金鑰不需要 currentOnStart
和 currentOnStop
,因為使用了 rememberUpdatedState
,其值在「組成」中一律不會變更。如未將 lifecycleOwner
做為參數傳遞,且參數發生變更,HomeScreen
會重組,但 DisposableEffect
不會遭到棄置並重新啟動。由於從該點開始,系統使用了錯誤的 lifecycleOwner
,因此會造成問題。
以常值為金鑰
您可以使用 true
這類常值做為「作用」金鑰,使其遵循呼叫站的生命週期。有效的用途確實存在,例如前述的 LaunchedEffect
範例。不過,在實際執行之前,請多考慮一下,確認您真的需要這麼做。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 狀態和 Jetpack Compose
- 適用於 Jetpack Compose 的 Kotlin
- 在 Compose 中使用 View