D15/ 为什麽 remember 是 composable function? - @Composable 是什麽

今天大概会聊到的范围

  • @Composable
  • compose compiler & runtime

爆个雷:今天的文章只会讲前半段,还不会回答 为什麽 remember 是 composable function? 

今天第 15 天,刚好走到铁人赛的一半,也算认真看 Compose 这个主题好一段时间了。但我一直有个疑惑:到底 @Composable 是什麽东西?

这个问题听起来可能很蠢,不就是我们在写 Compose UI 时,那些 UI element 都要加上 Composable ,所以 @Composable 应该是标示某一个 function 将会被视为 View 对吧?

一开始我也是这样想,直到我看到 remember 的定义:

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

等等!remember 为什麽是一个 composable function? 他不是 UI element 啊?

也许,remember 会操控到 UI 的内容勉强还说得过去。但是另一个例子:当我们需要 dp/sp/px 大小单位转换时,我们需要 LocalDenesity.current,但 LocalDensity.current 的 getter 长这个样子

inline val current: T
    @ReadOnlyComposable
    @Composable
    get() = currentComposer.consume(this)

为什麽你也是 composable function ???

@Composable 这个 Annotation 做了什麽

简单来说,@Compoable 这个 annotation 改变了这个 function 的性质。有一个很好的比喻的就是 suspend function ,加上 suspend 关键字的 function 会有与一般不同的行为,suspend function 只能在特定的 scope 或是别的 suspend function 中呼叫。@Composable 也是,加上 @Composable 这个 annotation 的 function 会有不同的行为,而且 @Composable 只能在别的 @Composable function  中呼叫。

@Composable
fun composableFunc()


@Composable
fun anotherCompFunc() {
    composableFunc() // ok
}


fun normalFunc(){
    composableFunc() // not allow
}

在我们用到 compose 相关的功能时,我们的专案都会执行 compose compiler。compose compiler 基本上是一个 gradle plugin 在 compile time 加入我们专案的建置。这个 plugin 会找到所有有标上 @Composable annotation 的东西 source 并且检查这些 composable 是否是在别的 composable function 中呼叫的 source

@Composalbe 并不会触发 annotation processor,而是可以视之为一个 keyword,就如同上面举例的 suspend 一样。他的目的是让 compiler 知道这个是 composable function,需要特别判断与处理

Entry point

既然 composable function 一定要在别的 composable function 内才能呼叫,那就会有一个鸡生蛋蛋生鸡的问题:第一个 composable function 要在哪里呼叫?
就和 suspend function 需要再 coroutine scope 中呼叫一样,composable function 也须要有一个 entry point。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeApp()
        }
    }
}

ComponentActivitysetContent function 就是一个 entry point。这个 function 本身不是 composable 但却接收了一个 composable function,因此,这个 function 内部并没有直接“呼叫”这个 content ( 这个 composable function ) 而是将它继续往下传递。经过很多层包装後,最终会

internal fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
    @Suppress("UNCHECKED_CAST")
    val realFn = composable as Function2<Composer, Int, Unit>
    realFn(composer, 1)
}

Composer

以下引用 Leland Richardson 的图。接下来大部分的的解释与理来都来自这个文章:https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd

找到 Composable function 的初始执行点了,那这个神秘的 Composer 到底做了什麽呢?在 compose 开始时,可以想像在 compose 启动开始时,Composer 会建立一个很大的空阵列。

https://ithelp.ithome.com.tw/upload/images/20210929/20141597wsamI8YYJX.png

当 Composer 走访各个 Composable 时,会一一将各个 Composable 放入这个阵列。并保留阵列尾端的空位

https://ithelp.ithome.com.tw/upload/images/20210929/20141597AVwq83IosC.png

当今天有某个 Composable 触发了 recomposition 时,composer 就会从头走访整个阵列。

https://ithelp.ithome.com.tw/upload/images/20210929/20141597Td6063t45L.png

Composer 取得每个位置的 composable 时,可以依照资料做决定:有可能决定完全不动(假设第一个方形完全没有改变),有可能决定改变其中的某个参数,但是不影响上下 ( ex. Text ) ( 假设圆形需要改变颜色 )。

https://ithelp.ithome.com.tw/upload/images/20210929/20141597JOjZ71OEDm.png

当今天某个 recomposition 需要触发 layout 改变增加 child composable 时,composer 会将尾端的空位直接移上来。

https://ithelp.ithome.com.tw/upload/images/20210929/20141597x6injL2RF3.png

从这里开始,重新将新的 composable 加入阵列中存放。

https://ithelp.ithome.com.tw/upload/images/20210929/20141597Bd588OnEAF.png

这样存放 composable 的好处,所有的行动(加入 composable、改变 composable 资料、删除 composable )都是固定时间的 (O(1)) 。唯一耗时的是移动空位 (O(n))。但移动空位通常是有重大 UI 改变时才会触发,而且每次触发都是一整个区块在做改变,因此,Compose UI 团队才会做这个设计和选择。


今天的问题还是没有被回答到。"为什麽 remember 是 composable function?"。但在回答这个之前,我们先碰触到了 Compose 整个 framework 运作核心的冰山一角。明天预计顺着这个逻辑,继续说明当今天有 State 在 composable tree 中时,composer 怎麽存放资料,state 又怎麽影响 composable。


Reference:


<<:  第14车厢-点开看更多?tableRWD应用篇

>>:  Day 14 JavaScript innerText vs textContent

Day 27 - styled-components 笔记2

Q_Q .. 把 props 传入 styled-components import styled...

DAY 20 『 连接 API 实作 - 天气 APP 』Part2

昨天介绍了如何抓取 API,今天来介绍如何根据 JSON 写一个 struct。 为了接收 API ...

Day1 — 前言:为什麽是 AVR?

或许看到本系列文章会产生的第一个疑问大概就是:「为什麽是 AVR?」。的确,现在潮潮都用 x86,不...

【day26】Span翻转TextView

好的,连假最後一天,我们来个小篇章,就是Span啦,Span可以做到的事情有很多,如 *更改特定位...

[Day 5] Course 1_Foundation - 资料分析工具及职涯探索

《30天带你上完 Google Data Analytics Certificate 课程》系列将...