今天大概會聊到的範圍
- recompose
在整個系列文章中,有提過不只一次的 recomposition。在 Day 15、16 時有特別提過抽象概念上是如何運作的。但通常,我都把它當成一個直覺的認知、一個常識,好像 recomposition 就是會發生、就是會如我們想像中執行。
但實際上,什麼時候會觸發 recomposition、它的機制又是怎麼運作呢?讓我們從一個範例開始看看:
@Composable
fun LogComp(name: String, value: Int = 0, content: @Composable () -> Unit) {
Log.d(TAG, "LogComp: name = $name, value = $value") // <-- 1
content()
}
@Composable
fun BuzzComp() {
var v by remember { mutableStateOf(0) }
Column {
LogComp("LogComp1") {
Log.d(TAG, "BuzzComp: log inside LogComp1")
LogComp("LogComp2") {
Log.d(TAG, "BuzzComp: log inside 2")
LogComp(name = "Third", value = v) {
Log.d(TAG, "BuzzComp: log inside third")
}
}
}
Button(onClick = { v++ }) {
Log.d(TAG, "BuzzComp: log inside button")
Text("Change value")
}
}
}
LogComp
來記錄 composable function 被呼叫的時間。一但被呼叫 LogCat 就會紀錄v
LogComp
彼此包裝彼此。在每一個 LogComp
的 content lambda 中,除了下一層的 LogComp
之外,還包含一個 LogCat logLogComp
放入 State v
v
+1請問,在點擊後會發生什麼事情呢?哪些 function 會被呼叫到呢?
--- init (composition) ---
LogComp: name = LogComp1, value = 0
BuzzComp: log inside LogComp1
LogComp: name = LogComp2, value = 0
BuzzComp: log inside LogComp2
LogComp: name = LogComp3, value = 0
BuzzComp: log inside LogComp3
BuzzComp: log inside button
--- clicked (trigger recomposition) ---
BuzzComp: log inside second
LogComp: name = LogComp3, value = 1
--- end ---
你猜對了嗎? 只有 second LogComp 的 content lambda 和 LogComp 3 本身會被呼叫到。
簡單來說,有對 State 的 value 有 access 的地方,當 value 改變時都會觸發 recomposition。
LogComp2
的 content lambda 要呼叫 LogComp3
時,有取用 v 並提供給 LogComp3
LogComp3
本身也有取用 value 來放在 Log 和 Text
中,所以會需要 recompositionRecompose scope 是 Compose 用來記錄哪些東西需要被 "重新整理" 的區塊劃分,它是每次觸發 recompose 的最小單位。他會紀錄哪些區塊有取用哪些 State。
在 Day 16 有提到,每個 Composable 前後會偷偷被加上 start / end 來將這個 composable 紀錄在中 Gap Buffer 中。其實那就是一個 Recompose Scope。每個回傳 Unit 的 non-inline composable function ,都會視為一個 recompose scope。
因此,每個 LogComp、Button、Text 甚至 LogComp 的 content lambda 都會產生一個 recompose scope。
當 State 改變時,有用到該 State 的 recompose scope 都會被 invalidate。並且會在下一個 frame 前執行 composition。
因此,LogComp3
本身有用到 State 所以會觸發 recomposition。LogComp2
的 content lambda 也有取用該 State ( 要給 LogComp3
) 所以也會跟著觸發。LogComp2
和 LogComp2
的 content lambda 是不同的 recompose scope,因此不會觸發 recompose。
LogComp2
如果我們將 value 使用的人從 LogComp3
改成 LogComp2
呢?
@Composable
fun BuzzComp() {
var v by remember { mutableStateOf(0) }
Column {
LogComp("LogComp1") {
Log.d(TAG, "BuzzComp: log inside LogComp1")
LogComp("LogComp2", value = v) { // <--- 換 LogComp2 用值
Log.d(TAG, "BuzzComp: log inside 2")
LogComp(name = "Third") {
Log.d(TAG, "BuzzComp: log inside third")
}
}
}
Button(onClick = { v++ }) {
Log.d(TAG, "BuzzComp: log inside button")
Text("Change value")
}
}
}
--- init (composition) ---
LogComp: name = LogComp1, value = 0
BuzzComp: log inside LogComp1
LogComp: name = LogComp2, value = 0
BuzzComp: log inside 2
LogComp: name = Third, value = 0
BuzzComp: log inside third
BuzzComp: log inside button
--- clicked (trigger recomposition) ---
BuzzComp: log inside LogComp1
LogComp: name = LogComp2, value = 1
--- end ---
一樣只有 LogComp1 的 content lambda 和 LogComp2 本身觸發 recompose。
在第二個範例中,即便 LogComp3
是放在 LogComp2
的 content lambda 中, LogComp2
的 content lambda 又在 LogComp2
function 中會被呼叫到。但實際上,因為 LogComp2
的 content lambda 與 LogComp3
和 LogComp2
屬於不同的 recompose scope 所以不會觸發 recomposition、不會再次被呼叫。
由此可知,compose 中的 function 行為其實和我們直覺想像的不盡相同。這又再次證實了為什麼不應該在 Composable function 中插入非 compose 相關的行為。因為我們無法確保這些 function 的觸發時機。反之,我們應該將這些 side effect 放到外部,或是適當的 composable function ( such as SideEffect
, DisposableEffect
.. etc )
Reference: