iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 19

D19/ 要權限的時後有 Launcher has not been initialized,怎麼辦? - SideEffect

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • 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 ) 都有可能產生問題。

Side Effect

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,不過感覺還沒辦法講得很明白,所以決定之後再分享。


上一篇
D18/ 怎麼在 Compose 中取得 Permission? - rememberLauncherForActivityResult
下一篇
D20/ 怎麼在 compose 與 non-compoe 間傳資料 - Compose Side-Effect part 2
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言