Thinking in Compose

Compose 终于迎来了第一个 Beta 版本,这意味着其 API 已经基本稳定了,所以这个时候开始学习 Compose 是最合适的。这篇主要是我在读了官方文档后做的笔记,关于 Compose 的开发模式以及需要注意的地方。

什么是 Compose

Jetpack Compose is a modern declarative UI Toolkit for Android. Compose makes it easier to write and maintain your app UI by providing a declarative API that allows you to render your app UI without imperatively mutating frontend views.

和 React-Native、Flutter、SwiftUI 一样,Compose 是一种声明式编程的 UI 框架。简单来说,就是让你可以直接使用 Kotlin 代码编写视图,而不需要每次从视图树中获取视图并进行修改。

声明式编程范式

传统方式的缺点

视图树中的每个组件都在内部维护着自己的状态(State)。在修改组件之前需要先找到该组件,然后调用相应的方法去修改状态(命令式)。这种方式不旦繁琐,而且容易产生 bug,比如修改视图状态时造成冲突。

Declarative UI model

只需声明组件,当状态发生变化时,重新生成并更新视图,在 Compose 框架中此过程叫做 Recomposition。不过,在实际情况下,为了减少性能损耗,Compose 框架会判断哪些组件需要更新,从而只更新需要更新的部分。

优点1:将可变的状态暴露成方法参数,从而可以和架构模式中一些组件(比如 ViewModel)结合起来使用:Compose 接受初始数据,当用户输入数据时将事件通知给 ViewModel 去处理,然后 ViewModel 再通知 Compose 重新对视图进行渲染。

优点2:动态性。可以充分发挥 Kotlin 语言的优势,只需要很少的代码就可以轻松写出灵活多变的视图结构。

Composable 函数的组成部分

1
2
3
4
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
  • Composable 函数必须使用 @Composable 注解。使用该注解告诉 Compose 编译器可以将该方法转换成 UI。
  • Composable 函数接收参数。
  • Composable 函数没有返回值。
  • Composable 函数运行具有等幂性(idempotent)、无副作用(side-effect free)、运行快的特点。
    • 等幂性即使用相同参数调用一次和调用多次的结果相同,不依赖全局变量或者随机结果函数。
    • 只修改 UI 而不产生任何 side-effects,即不造成除 composeable 函数以外的任何变化,比如修改成员属性或者全局变量的值、读写数据库、更改 ViewModel 的状态等等。

Recomposition

传统开发方式中,当我们需要修改视图的时候,会先找到该视图然后进行修改,但是在 Compose 框架中,我们通过重新调用 composable 函数(传入新数据),然后由 Compose 框架对视图进行修改,此过程就叫做 recomposition

之前说过,重新生成整个视图树代价高昂,所以出于性能考虑,Compose 框架只会对发生变化的部分进行修改,此过程也被叫做 intelligent recomposition

正因如此,在 recomposition 的过程中,一些函数、lambda 表达式有可能会被跳过,也是因为这个原因,我们才不能依赖任何 side-effects,比如:

  • 对一个共享对象的属性值进行修改
  • 更新 ViewModel 中的 Observable
  • 更新 SharedPreference

另外,composable 函数可能被多次执行,甚至每一帧执行一次(比如执行动画的时候),所以如果你的 composable 函数中包含一些耗时操作(比如读写 SharedPreference),最好放到子线程中进行,比如使用 Coroutine 并将结果作为参数传递到 composable 函数中。

除此之外,在使用 Compose 的过程中还需要注意以下这些问题。

Composable 函数不一定会按顺序执行

通常情况下,composable 函数的确会按顺序执行,但是当一个 composable 函数中包含其它 composable 函数时,就不一定是按顺序执行的了。Compose 框架会识别出具有更高优先级的 UI 组件,并对它们优先进行渲染。所以,当嵌套使用 composable 函数的时候,要记得每个 composable 函数必须是独立的,不能依赖其它 composable 函数的执行状态。

Composable 函数可能同步执行

在 recomposition 过程中,为了加快运行速度,多个 composable 函数可能会同时被执行。另外,这也意味着某些 composable 函数会被放到后台子线程中执行。比如,当 composable 函数参数中包含 ViewModel 中的方法,那么该方法有可能在多个线程中被执行了多次。所以,我们不能在 composable 中引入 side-effects。

Recomposition 会尽可能多地被跳过

正如之前所说,Compose 只会更新需要更新的部分,剩下的部分会被跳过。

Recomposition 是乐观的并且可能会取消并重新执行

每当参数发生变化的时候,recomposition 就会发生。乐观的意思是 Compose 通常会认为在接收到新的参数变化之前,已经完成了上一次的 recomposition,所以,当参数发生变化时,如果上一次 recomposition 还没完成,Compose 会优先取消掉它并使用新的参数重新执行 recomposition。

同样的,如果你在 composable 函数中包含 side-effects,比如修改了某个全局参数,那么该参数有可能在 recompositon 取消并重新执行的过程中被修改了多次。所以,再次提醒,一定要保持 composable 函数的等幂性 idempotent 和无 side-effects。

Composable 函数可能被频繁执行

在某些情况下,composable 函数可能每帧都被执行一次,比如使用动画的时候。所以,记住不要在 composable 函数中执行诸如 IO 等耗时的操作。一般的做法是将数据定义在 composable 函数的参数中,如果获取数据比较耗时则应该放在子线程中,然后使用 MutableState 或者 LiveData 对数据进行接收。

链接:Thinking in Compose