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:


<<:  人脸辨识-day23 资料的型态

>>:  Re: 新手让网页 act 起来: Day23 - useCallback 与 React.memo

Day6 职训(机器学习与资料分析工程师培训班): PHP连接SQL

上午: Python程序设计 延续昨日课程,今日从流程控制开始,课程中老师也有出几个练习题让同学试着...

GitHub Project Board - 看板方法

GitHub 目前提供的 Project 功能为 Board (看板),在撰写这篇文章时,GitH...

纵深防御架构有效性检视

资安制度说白了就是企业或机构营运的日常管理,套用 ISMS 框架也只是为了满足内部或外部需求,视产业...

Day9 Let's ODOO: View(2) Structure

我们今天来介绍一份View的基本结构 以昨日的Form作为例子: <odoo> <...

电源供应

电源是一种向电力负载提供电力的电气设备。电源的主要功能是将电流从源头转换成正确的电压、电流和频率,为...