今天的文章中,我們要來實作彈出 ModalBottomSheet
的功能。目前,如果嘗試點擊日記上的選單,不會觸發任何動作。
此系列文章是以我的業餘專案: Kimoji 作為範例。
這款以純 Jetpack Compose 撰寫的 side project,已經在 Google Play 上架。 歡迎試玩!
立馬下載
在 KimojiApp.kt
檔案中的 DiaryScreen
composable 裡,在 onMoreClicked
callback 中需要開啟 ModalBottomSheet
。
@Composable
fun KimojiApp(
modifier: Modifier = Modifier
) {
val appState = rememberKimojiAppState()
ModalBottomSheetLayout(
sheetContent = {
EditSheet()
},
sheetState = appState.modalBottomSheetState
) {
Scaffold(
modifier = Modifier.statusBarsPadding(),
scaffoldState = appState.scaffoldState
) {
DiaryScreen(
modifier = modifier,
onMoreClicked = {
//TODO: Show [ModalBottomSheet]
}
)
}
}
}
我們在 KimojiApp
中有一個包含 ModalBottomSheetState
的 KimojiAppState
。ModalBottomSheetState
有提供一些函式可以讓我們透過程式開啟和關閉 ModalBottomSheet
。不過,如果我們嘗試在 onMoreClicked
callback 中寫入 appState.modalBottomSheetState.show()
,就會收到錯誤訊息!這是因為 show()
是一個 suspend function。我們又再次回到 coroutines 的世界了。
某些 Compose API 可以讓我們安全地從 UI layer 呼叫 coroutines (例如在昨天的文章中介紹的 LaunchEffect
),而某些 Compose API 屬於 suspend function (例如今天的文章要實作的開啟 ModalBottomSheet
的 API)。Suspend function 除了能執行非同步程式碼外,還有助於表示隨著時間演變的概念。開啟 ModalBottomSheet
需要時間和動畫,因此很適合透過 suspend function 完全體現出來。這是因為 suspend function 可以在被呼叫的位置暫停執行 coroutine,直到函式完成並恢復執行為止。
我們必須在 coroutine 裡面呼叫 appState.modalBottomSheetState.show()
。可以怎麼做呢?我們來看一下 onMoreClicked
這個簡單的 callback function:
onMoreClicked
並未在 coroutine context 中執行,所以我們不能在它裡面呼叫 suspend functions。LaunchedEffect
,因為 onMoreClicked
不在 Composition 中,所以無法在它裡面呼叫 composables。我們希望能夠 launch coroutine,但該使用什麼 scope 呢?在理想情況下,我們希望 CoroutineScope
能夠 follow call-site 的生命週期。方法就是使用 rememberCoroutineScope
API。離開 Composition 後,scope 就會自動 cancel。有了這個 scope,即使不在 Composition 中 (例如在 onMoreClicked
callback中),也能啟動 coroutines。
@Composable
fun KimojiApp(
modifier: Modifier = Modifier
) {
val appState = rememberKimojiAppState()
ModalBottomSheetLayout(
sheetContent = {
EditSheet()
},
sheetState = appState.modalBottomSheetState
) {
Scaffold(
modifier = Modifier.statusBarsPadding(),
scaffoldState = appState.scaffoldState
) {
val scope = rememberCoroutineScope()
DiaryScreen(
modifier = modifier,
onMoreClicked = {
scope.launch {
appState.modalBottomSheetState.show()
}
}
)
}
}
}
如果我們把 app 跑起來,只要點擊日記上的選單圖示就會開啟 ModalBottomSheet
。
由於一般的 callback 在 Composition 以外,我們必須在 callback 中建立 CoroutineScope
才能呼叫 suspend functions,因此在這種情況下無法使用 LaunchedEffect
。
回想一下昨天我們使用 LaunchedEffect
來顯示 landing screen 的 code,我們可以在不使用 LaunchedEffect
的情況下,使用 rememberCoroutineScope
並呼叫 scope.launch { delay(); onTimeout(); }
嗎?
我們本來可以這樣做,也似乎可行,但這樣並不正確。如同我們前幾天在 Compose Recomposition 文章中所述,Composable function 可能會很頻繁地被執行。當呼叫 composable 並進入 Composition 時,LaunchedEffect
保證會執行 side-effect。如果在 LandingScreen
的 body 中使用 rememberCoroutineScope
和 scope.launch
,則每次 Compose 呼叫 LandingScreen
時,不論該呼叫是否為 initial Composition,coroutine 都會被執行。因此,我們不僅會耗費資源,而且基本上會讓執行這個 side-effect 的情況不受控。
此系列文章是以我的業餘專案:Kimoji 為範例。
Kimoji 是一款心情日記 App,讓你用可愛的 emoji 來撰寫你的心情日記。現在就來試試這款設計精美的微日記吧!
立馬下載
Reference: https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects