今天大概會聊到的範圍
- SideEffect
- DisposableEffect
今天要講的東西是 Side-Effect,很多場景下都有更好的 solution。以下多是為了舉例而寫出來的程式,請在實際使用前思考正確架構。
前情提要,上一篇有講到如果我們在判斷沒有 persmission 時,就主動去 launch 並且試圖取得 permission :
@Composable
fun CameraScreen() {
// 取得 context
val context = LocalContext.current
// 檢查完 permission 後將答案存在 remember 中
var hasPermission by remember {
mutableStateOf(checkPermissionsGranted(context))
}
val permissionRequester = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
hasPermission = isGranted
}
if (hasPermission) {
CameraView()
} else {
permissionRequester.launch(Manifest.permission.CAMERA) // error
}
}
程式會報錯,會出現 Launcher has not been initialized 的錯誤。
原因是這樣的,composable function 和一般 function 不同。他們的執行時間、執行次數都是由 compose-runtime 所決定。以這個例子為例,在 launcher ( permissionRequester
) 還沒準備好的時候,CameraScreen
就會先被執行了好幾次,因此會出現錯誤。
在 Compose 中,每一個 composable function 都可以視為一個 "接收參數並產生畫面" 的 function。在這個定義下,所有與這個流程無關的資料改變、行為都屬於 "副作用" side-effect。
舉個例: ( anti-pattern 請勿學習 )
var count = 0
@Composable
fun SideEffectAntiPattern() {
count ++
}
因為我們無法控制 composable function 的執行次數、生命週期。當 composable function 內有異動到外部的資料、或是發出異步的 function (例如 network request ) 都有可能產生問題。
SideEffect
是一個可以讓我們用來明確定義 side effect 效果的 composable function。 他可以讓我們要執行的事情在 composition 完成後執行。
var count = 0
@Composable
fun SideEffectComposable() {
SideEffect {
count ++
}
}
SideEffect
還可以用來將 compose state 傳遞給無法使用 compose state 的物件:
@Composable
fun SideEffectComposable(window: Window) {
var color by remember { mutableStateOf(Color.Blue) }
SideEffect {
window.statusBarColor = color.toArgb()
}
}
有些時後,我們不僅是要在 composition 完成時觸發事件,還要再 composable 要被回收時做一些 clean up 的動作。這時我們可以使用和 SideEffect
很類似的 DisposableEffect
val lifecycleOwner = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycleOwner) {
val lifecycle = lifecycleOwner
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
// do things
}
Lifecycle.Event.ON_RESUME -> {
// do things
}
Lifecycle.Event.ON_DESTROY -> {
// do things
}
}
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
DisposableEffect
可以接收(也可以不提供)一個 key。當 composition 完成時,DisposableEffect
後面的 lambda 會執行。在 lambda 的最後,需要透過 onDispose
function 產生一個 DisposableEffectResult
。當 key 改變或是當 composable 被清除掉時,DisposableEffectResult
中的行為就會觸發。
透過 SideEffect 和 DisposableEffect 可以間接碰觸到 Composable 的生命週期,在正確的時機點執行。但這些可能都不是最好的做法(case by case),在 Compose 中,最好還是讓 composable function 維持只有處理畫面的部分,行為等可以往上提升到適當的階層,或是透過 view model 等角色拉到外部去觸發。
回到取得權限,因為我們需要在 composition 完成後執行 launcher.launch ,我們只需要簡單將 launch 行為透過 SideEffect 包裝起來就可以了。
if (hasPermission) {
CameraView()
} else {
SideEffect {
permissionRequester.launch(Manifest.permission.CAMERA)
}
}
今天的文章最後感覺沒什麼收尾,那是因為我在研究 SideEffect 的過程,還有看到一系列的 state & effect API,不過感覺還沒辦法講得很明白,所以決定之後再分享。