iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Mobile Development

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

D24 / 什麼時候我的 Composable function 會重新被呼叫 - recompose

今天大概會聊到的範圍

  • 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")
            
        }
    }
}
  1. 我們透過自定義的 LogComp 來記錄 composable function 被呼叫的時間。一但被呼叫 LogCat 就會紀錄
  2. 在主要畫面中,建立一個 State v
  3. 在畫面中,建立三個 LogComp 彼此包裝彼此。在每一個 LogComp 的 content lambda 中,除了下一層的 LogComp 之外,還包含一個 LogCat log
  4. 在第三層的 LogComp 放入 State v
  5. 另外建立一個按鈕,點擊後就會觸發 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。

  1. LogComp2 的 content lambda 要呼叫 LogComp3 時,有取用 v 並提供給 LogComp3
  2. LogComp3 本身也有取用 value 來放在 Log 和 Text 中,所以會需要 recomposition

Recompose Scope

Recompose 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 ) 所以也會跟著觸發。LogComp2LogComp2 的 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。

再次提到 Side Effect

在第二個範例中,即便 LogComp3 是放在 LogComp2 的 content lambda 中, LogComp2 的 content lambda 又在 LogComp2 function 中會被呼叫到。但實際上,因為 LogComp2 的 content lambda 與 LogComp3LogComp2 屬於不同的 recompose scope 所以不會觸發 recomposition、不會再次被呼叫。

由此可知,compose 中的 function 行為其實和我們直覺想像的不盡相同。這又再次證實了為什麼不應該在 Composable function 中插入非 compose 相關的行為。因為我們無法確保這些 function 的觸發時機。反之,我們應該將這些 side effect 放到外部,或是適當的 composable function ( such as SideEffect, DisposableEffect .. etc )


Reference:


上一篇
D23/ MaterialTheme 怎麼運作的? - CompositionLocal
下一篇
D25 / 為什麼 State 改變會觸發 recomposition - State & Snapshot system
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言