在 Kotlin Multiplatform (KMP) Compose 中,“effect functions”(或“effect handlers”)是專門的可組合函數,用于在 UI 中管理副作用。
在 Compose 中,可組合函數應該是“純”的和聲明式的。這意味著它們應該理想地只接受輸入并生成 UI,而不引起其作用域之外的任何變化。然而,現實世界中的應用程序通常需要與“外部世界”進行交互,例如:
- 網絡請求:從 API 獲取數據。
- 數據庫操作:保存或加載數據。
- 日志/分析:將數據發送到外部服務。
- 管理生命周期依賴的資源:注冊/注銷監聽,器管理訂閱。
- 觸發一次性 UI 事件:顯示一個 Snackbar,導航到另一個屏幕。
- 更新非 Compose 狀態:與 ViewModel 或其他非 Compose 狀態持有者交互。
這些操作被稱為“副作用”,因為它們改變了應用程序在即時 UI 渲染過程之外的狀態。如果處理不當,它們可能會導致不可預測的行為、錯誤或性能問題。
為什么需要 Effect Functions?
Compose 的重組模型意味著可組合函數可能會頻繁且以任意順序被調用。如果將副作用直接放入普通的可組合函數中,它們可能會被重復執行或在不適當的時間執行,從而導致問題。Effect functions 提供了一種受控且具有生命周期感知能力的方式來執行這些副作用。
KMP Compose 中最常見的 effect functions:
- LaunchedEffect
- 用途:啟動一個與可組合生命周期綁定的協程(異步操作),并依賴于一組“keys”。
- 何時使用:當需要在可組合進入組合時執行掛起函數(例如,網絡請求、數據庫調用),或者當特定“key”(狀態變量或參數)發生變化時。如果 key 發生變化,LaunchedEffect 啟動的前一個協程將自動取消,并啟動一個新的協程。
- 示例代碼:
@Composablefun UserProfileScreen(userId: String) {var userData by remember { mutableStateOf<UserData?>(null) }LaunchedEffect(key1 = userId) {userData = fetchUserData(userId)}if (userData != null) {Text(text = "Name: ${userData?.name}")Text(text = "Email: ${userData?.email}")} else {CircularProgressIndicator()}}suspend fun fetchUserData(userId: String): UserData {// 模擬網絡請求delay(1000)return UserData(name = "John Doe", email = "john.doe@example.com")}data class UserData(val name: String, val email: String)```2. DisposableEffect- 用途:執行與可組合生命周期和 keys 相關聯的設置和清理操作。
- 何時使用:當需要管理需要顯式清理的資源時。它提供了一個 onDispose 塊,當可組合離開組合或者其 keys 發生變化時(意味著 effect 被“處置”,可能會設置一個新的 effect)執行。
- 示例代碼:
@Composable
fun LocationScreen() {
var location by remember { mutableStateOf<String?>(null) }
DisposableEffect(Unit) {val listener = object : LocationListener {override fun onLocationChanged(loc: Location) {location = "${loc.latitude}, ${loc.longitude}"}}// 注冊監聽器registerLocationListener(listener)onDispose {// 注銷監聽器unregisterLocationListener(listener)}}if (location != null) {Text(text = "Current Location: $location")} else {Text(text = "No location available")}
}
- SideEffect
-
用途:在可組合函數成功重組時運行非掛起代碼。
-
何時使用:在成功重組后,將 Compose 狀態與外部、非 Compose 管理的對象同步。它確保代碼在 UI 更新后運行。
-
示例代碼:
@Composable
fun LoggingScreen() {
var counter by remember { mutableStateOf(0) }SideEffect {log("Counter value is now $counter")}Button(onClick = { counter++ }) {Text(text = "Increment")}
}
fun log(message: String) {
println(“LOG: $message”)
}```
- rememberCoroutineScope
-
用途:獲取一個與可組合生命周期綁定的 CoroutineScope。
-
何時使用:當需要從 Compose 作用域之外的回調(例如,Button 的 onClick lambda)中啟動協程,但仍然希望當可組合離開組合時取消協程。可以在回調中手動調用 .launch { … }。
-
示例代碼:
@Composable
fun DelayedActionScreen() {
var message by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()Button(onClick = {scope.launch {delay(2000)message = "Action completed after 2 seconds"}}) {Text(text = "Perform Delayed Action")}if (message != null) {Text(text = message!!)}
}
- produceState
- 用途:將非 Compose 可觀察狀態(例如,基于回調的 API,Android 中的 Flow 或 LiveData)轉換為 Compose State,以便其他可組合函數可以觀察它。
- 何時使用:當需要將現有的命令式 API 或數據流集成到 Compose UI 中時。它啟動一個協程來隨時間更新 State。
- 示例代碼:
@Composablefun DataScreen() {val data = produceState(initialValue = "Loading...") {val flow = fetchDataFlow()collect {value = it}}Text(text = data.value)}fun fetchDataFlow(): Flow<String> {return flow {delay(1000)emit("Data loaded")}}```6. derivedStateOf- 用途:從其他 State 對象創建一個 State,但只有當派生值實際發生變化時才會重組,而不是只要底層狀態發生變化就重組。
- 何時使用:當基于其他狀態有復雜的計算或轉換,并且只想在派生值的結果發生變化時才觸發重組時,用于性能優化。
- 示例代碼:
@Composable
fun DerivedStateScreen() {var x by remember { mutableStateOf(0) }var y by remember { mutableStateOf(0) }val derivedState = derivedStateOf {x + y}Text(text = "Derived State: ${derivedState.value}")Button(onClick = { x++ }) {Text(text = "Increment x")}Button(onClick = { y++ }) {Text(text = "Increment y")}
}
7. snapshotFlow- 用途:將 Compose State 對象轉換為 Kotlin Flow。
- 何時使用:當想利用 Kotlin Flows 的強大功能(例如,map、filter、debounce 等操作符)與 Compose State 時。
- 示例代碼:
@Composable
fun DebouncedSearchScreen() {
var query by remember { mutableStateOf(“”) }
val flow = snapshotFlow { query }.debounce(300).collect { performSearch(it)}TextField(value = query, onValueChange = { query = it })
}fun performSearch(query: String) {println("Performing search for: $query")
}