現代化 Android — Jetpack Compose Part 2 — Rendering And State
上一篇簡單的介紹了 Compose 和用法,但現實世界中其實沒有這麼簡單,不會只是單單的呈現預設的文字或動作,往往還需要根據使用者互動來更新畫面,像是點擊按鈕、呼叫 API 更新資料等。
這篇要來介紹在 Compose 的世界裡要怎麼讓 Composable 記憶狀態以及和使用者互動。
那該怎麼做?
透過 「State」來達成
在了解 State 之前我們要先知道 Compose 是如何渲染(Rendering)的,在官方文件中這是分成兩個章節,但我個人覺得要先了解 Rendering 在看 state 會比較清楚。
View 渲染(Rendering)
先回顧目前的 View 是如何 Rendering 的?我們知道一個 View 他會經過這樣的過程:
onMeasure -> onLayout -> onDraw
然後如果要再更新 View 時可以呼叫 invalidate()
Compose 怎麼做?
以下擷取官方的說明和圖片來解釋
When Jetpack Compose runs your composables for the first time, during initial composition, it will keep track of the composables that you call to describe your UI in a Composition.
Then, when the state of your app changes, Jetpack Compose schedules a recomposition. Recomposition is when Jetpack Compose re-executes the composables that may have changed in response to state changes, and then updates the Composition to reflect any changes.
A Composition can only be produced by an initial composition and updated by recomposition. The only way to modify a Composition is through recomposition.
上面說了這麼多,總結一下就是:
- Compose 第一次跑時會根據你的 composables 組合出 UI
- 之後變動時會根據「資料變動」去 re-composition
- 要重新組合 UI 唯一方式就是重新組合 UI
所以每次只要資料變動,Composition 就要重來一遍?
對,也不對。根據官方文件內有提到:
If a composable is already in the Composition, it can skip recomposition if all the inputs are stable and haven’t changed.
Compose 它其實很聰明,如果它發現裡面的每個 composable 都是相同的內容沒被更動過,那它就會直接跳過。
既然我們了解 compose 是透過 composition 和 recomposition 來組合 UI,接下來就可以繼續往下看如何讓 compose 觸發 recomposition。
State 是 Compose 內一種可以記憶狀態在 memory 中的值,他可以記憶任何的值。
假設我今天有一個 Compose (取至官方)
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
如果是上面這段程式碼,我想要動態的改變 Text 的值,這時候就要加上 state ,改為以下 (黑粗體字):
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
透過 remember + state,當 onValueChange 賦值給 name 時,就達成了上面提到的當資料有變動時要 recomposition 的條件,所以 compose 會去找到這個組合中不一樣的地方並且改變 Text 內的值。
這樣我們就完成了 Stateful 的 UI,State 有三種宣告方式:
- val mutableState = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (value, setValue) = remember { mutableStateOf(default) }
更多的練習和資訊可以參考 code lab:
https://developer.android.com/codelabs/jetpack-compose-state#0
最後還要提到很重要的 Side-Effects,為什麼 Side-Effects 很重要?因為上面提到了 compose 會因「資料變動」觸發 recomposition,也因為這個原因,所以要特別注意「資料變動」的時機點,像是:
- 不能夠無限次執行
- 耗時執行(運算、網路操作)
- 不明確的時機點 (像是全域變數)
但現實世界中我們可能沒辦法完全遵守這些規則,所以至少要做到以下幾點才是合理的 side effect:
- 執行時機明確,例如在
Recomposition
时,或者在onMount
等 - 執行次數明確,不會再不需要的時候被執行,例如每幾秒在背景計算資料時也觸發
Recomposition
- 不會有 Memory Leak,有始有終,例如 fragment 消失時不應該繼續觸發 compose
要能夠正確的使用 side effect,我們還需要先了解 composable 的 lifecycle
官方有提供許多對應的 side effect 處理方式,像是 DisposableEffect、SideEffect、remember、LaunchedEffect…等
而我們上方介紹的就是使用了 remember 來操作,了解 composable lifecycle 搭配正確處理 side effect,才能避免 memory leak 和效能出現問題哦!
結語
這篇介紹了 compose 如何更新 UI,和應該怎麼做,不過總覺得和現實世界還是差了一些,要如何讓 compose 與 viewModel 互動?或是要怎麼在專案中開始我的第一個 Compose,這些都是初學 compose 時很想瞭解的事情,我們在下一篇當中待續。