現代化 Android — Jetpack Compose Part 2 — Rendering And State

Roy Huang
Pinkoi Engineering
Published in
8 min readOct 28, 2021
Photo by Rodion Kutsaev on Unsplash

上一篇簡單的介紹了 Compose 和用法,但現實世界中其實沒有這麼簡單,不會只是單單的呈現預設的文字或動作,往往還需要根據使用者互動來更新畫面,像是點擊按鈕、呼叫 API 更新資料等。

這篇要來介紹在 Compose 的世界裡要怎麼讓 Composable 記憶狀態以及和使用者互動。

那該怎麼做?

透過 「State」來達成

在了解 State 之前我們要先知道 Compose 是如何渲染(Rendering)的,在官方文件中這是分成兩個章節,但我個人覺得要先了解 Rendering 在看 state 會比較清楚。

View 渲染(Rendering)

先回顧目前的 View 是如何 Rendering 的?我們知道一個 View 他會經過這樣的過程:

onMeasure -> onLayout -> onDraw

然後如果要再更新 View 時可以呼叫 invalidate()

Compose 怎麼做?

以下擷取官方的說明和圖片來解釋

https://developer.android.com/jetpack/compose/lifecycle

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

composable lifecycle

官方有提供許多對應的 side effect 處理方式,像是 DisposableEffect、SideEffect、remember、LaunchedEffect…等

而我們上方介紹的就是使用了 remember 來操作,了解 composable lifecycle 搭配正確處理 side effect,才能避免 memory leak 和效能出現問題哦!

結語

這篇介紹了 compose 如何更新 UI,和應該怎麼做,不過總覺得和現實世界還是差了一些,要如何讓 compose 與 viewModel 互動?或是要怎麼在專案中開始我的第一個 Compose,這些都是初學 compose 時很想瞭解的事情,我們在下一篇當中待續。

工商時間

Pinkoi 強力徵才中,如果你對做好一個產品充滿熱情,對技術成長充滿渴望,Pinkoi APP 團隊是一個能讓你充分發揮的舞台,期待你的加入💪

了解更多 Pinkoi APP Team:

--

--