谷歌开发者大会扔物线演讲原稿整理:Jetpack Compose

大家好,我是扔物线朱凯。前两天,我在 GDG DevFest 2020 的 Android Day 做了一次面向全国 Android 工程师的技术分享直播,主题是 Android 最新的 UI 框架 Jetpack Compose。今天我把演讲的内容整理成文字 + 图片的形式在这里发出来,各位自取吧。

另外现场视频我也已经剪辑了出来(大概 35 分钟),要看视频的扫码加我的小助理,让她把视频发给你。

话不多说,以下是整理稿原文。大家如果觉得有用,请帮忙点赞和转发呀。

啥?Compose 是啥?这是 Android 会场吗?

我们今天的主题大家都看到了:Jetpack Compose。看到这个主题,大家的反应我猜是不一样的:有人会说哎呀太好了,早就想学 Compose 了;

相反呢也有一些人会一头雾水:这是啥?我走错片场了?有的人知道很久了,有的人还没听过。这是为什么呢,因为 Compose 虽然早就公布了,但到现在还没有发布。

Compose 是 Jetpack 中的一个新成员,是 Android 团队在 2019 年 I/O 大会上公布的新的 UI 库。不过虽然 2019 年 I/O 大会就公布了,可是到现在一年半了才到 alpha 阶段,而直到明年也就是 2021 年中或者下半年才会上正式版,也就是正式的发布。所以如果你没听过 Compose,或者只是听过这个名字但不知道它是什么,很正常。

那么问题就来了:

这才 Alpha 阶段怎么就开始推广了?大家要知道,我们在这里分享的主题,都是经过慎重挑选的,给大家讲的都是我们认为该讲的东西。那为什么我要来讲一个还在 alpha 的东西呢?

  • 因为这是一个特别大的东西。从项目规模上来讲,Compose 差不多是 Android 团队这几年在开发者这边除了宣布对 Kotlin 的官方支持之外,动作最大的一件事了。
  • Compose 这个库——或者说这个框架——它是重新定义了 Android 上 UI 的写法。如果我们用它来写界面,我们的开发效率和我们能够达到的能力都会提升很多。用官方的说法就是,它「让困难的事情变简单,让不可能的事情变得有可能」。
  • 但是,就像我说的,它换了一套全新的 UI 写法,是真的 UI 的写法一整套完全替换了。那这就意味着什么呢?学习成本。所以虽然明年才会正式发布,但我们需要提早做准备,才能在它发布之后可以第一时间用上。
  • 那么说到学习,大家就会有疑问了:
  • 首先是:怎么学啊?

怎么学?我这不是来给大家讲了嘛。

另外我接下来的一段时间也会视频形式发布更多的 Jetpack Compose 的分享,所以你们记得关注我,搜索扔物线就是我。

  • 另外,大家还会有一个更加尖锐的问题:我凭什么学啊?是吧?好气啊,怎么又来个新东西,不学!凭什么学?其实就一个原因:它太好了。或者我这么说:等我今天讲完这几十分钟,等你明白 Compose 有什么不同,你很可能就想学了。当然气可能还是会气啊,因为毕竟学不动了吗,烦是吧,但是你会想要学它的。

不过我也刚说了,这是一套非常大的框架,所以今天这几十分钟把它讲透了也是不可能的,今天我讲的核心内容是相对于具体的写法来说更重要的——Compose 整体是什么,或者熟悉我的人知道我喜欢用的一个词:扒皮。

今天主要是把 Compose 的皮扒了,给它做一个清晰的画像。是会讲到代码的,但是对于入门来说,Compose 需要一个专门的认识过程。大家都知道协程,如果你听过我讲协程,应该记得我是有一个对于协程是什么的专门讲解的,对吧?这个过程对于后面学会协程是必不可少的。而 Compose 同理,它很大,大到了你需要先对它的概念有一个了解才能更好地学会它。

Compose 怎么写?

那么我们就从 Compose 长什么样说起,也就是它的代码是怎么写的。

Compose 的代码是只能用 Kotlin 写的,它的每个控件都是一个函数调用。比如你要显示一块文字,就这么写:

这看起来还挺简单吧?好像是就调用构造函数创建了个对象而已,但其实这么写就可以显示一块文字出来了。

不过——这其实并没有创建对象,这个 Text() 是一个普通的函数。如果你跳到它的定义就会发现,这个 Text 并不是一个构造函数,而是一个普通函数。

哎?普通函数用大写开头干嘛?这不符合规矩啊!其实原因很简单,Compose 规定用大写字母开头的命名方式,这样可以有更好的辨识度:一看就知道,哦,这是个 Compose 的函数。所以说它的这种「不合规矩」,恰恰是 Compose 自己的特殊规矩。

Text 不是 TextView

那么这个 Text() 到底是个什么东西呢?是个 TextView 吗?是 TextView 到了 Compose 里就改名用 Text 来表示了?或者说,这个 Text 的底层实现是用的 TextView

都不是的,它俩完全不是一个东西。而且并不是因为它俩的名字不一样,在 Compose 里有一个叫 Button 的函数,我们平时写 UI 也有个控件叫 Button 对吧?但这两个 Button 也不是一个东西。而且不仅不是一个东西,它们连概念都不一样了:
在 Compose 里的 Button ,里面可以包东西——包文字也就是 Text 、包图片,什么都能包。这个所谓的「包」,就是里面加上子元素,相当于我们传统 UI 系统的子 View:

所以不仅 Compose 的 Text() 不是 TextView,Compose 的 Button 更不是我们平时用的 Button。

那么说到这儿大家发现没有:Compose 和我们平时用的那个 View 和 ViewGroup 的系统,不仅是写法不一样,它并不是仅仅创造了一套全新的写法。

Compose 并不是对 View 和 ViewGroup 这套系统做了个上层包装来让写法更简单,而是完全抛弃了这套系统,自己把整个的渲染机制从里到外做了个全新的。

大家知道 Flutter 吗?Flutter 是个跨平台的 UI 框架对吧?它为什么火?因为它不仅跨平台,而且流畅。它流畅就是因为它没有用原生控件,而是从底层利用更下层的渲染 API——比如对 Android 来说就是 Canvas 的 API——独立做了一套渲染逻辑。这套渲染逻辑直接和上层的 Flutter API 对接,而不是使用原生控件来间接渲染,这是没有性能损耗的,所以流畅。传统的跨平台框架都是用原生控件来做拼装,这样他的渲染要经过一层逻辑的转换,性能就会比原生差一些。

而 Compose,也是像 Flutter 这样,在创造了新的 UI 写法的同时,也创造了与这套写法对应的整个渲染核心——当然了不只是渲染,还有布局和触摸,全是重新写的。

所以对于 Jetpack Compose 的学习,你就算 View 那套系统不熟悉,学习起来也几乎没影响。

但……这也同时意味着,就算是熟悉了 View 那套系统,你依然需要把整个 Compose 的框架全学一遍,因为它从头到尾都是个新东西。

@Composable

好我们继续说。刚才说 Text() 是个普通函数对吧?其实说它是个普通函数,它其实也有不普通的地方,比如上面的 code>@Composable 注解。

那么我们现在就来说一下这个 code>@Composable 。实际上,这个 code>@Composable 是 Compose 里你会遇到和用到最多的一行代码,因为 Compose 提供的这些界面函数全都是带有这个注解的,而且所有的这些带有 code>@Composable 的函数,也全都需要在另一个这种 Composable 函数的里面被调用。

实质上,这个 code>@Composable 就是 Compose 这个框架最关键的魔法入口:在编译过程中,Compose 会对于加了这个注解的函数做出不一样的编译逻辑,让这些函数可以在 App 运行时变成我们的界面,正确地显示。

  • 说到干预编译过程,我们很容易想到 Annotation Processor,也就是注解处理器。不过它利用的不是 Annotation Processor,而是编译器插件,直接干预的编译过程。
  • 要说干预编译过程和 Annotation Processor 有什么区别,那主要就是 AP 是修改 Kotlin 代码,而编译器插件是直接修改的字节码的输出逻辑。
  • 跟它更像的其实是另一个东西:协程的 suspend 关键字。只不过Compose 选择的不是关键字方案,而是注解。
  • 为什么不用关键字?

因为它不配!

关键字是谁用的?是语言用的,是整个语言层面通用的东西。Compose 是语言层面的东西吗?当然不是了,它是一个 Android 的 UI 库而已。如果用关键字方案来做 Compose,那就相当于污染了 Kotlin 了。

所以,这就是 Compose:调用一个带有 code>@Composable 注解的函数,来显示出一个元素;而拼凑一下,调用一大堆嵌套的这样的函数,就是一个复杂的、精细的界面。那么这么看来,这个 Compose 还是可以用的嘛!

但是……它好在哪呢?

Compose 好在哪

也就是说,明明已经有一套用了很久的 UI 框架了,Android 为什么要推出一个新的呢?这个应该是相对于「怎么用」,大家更关心的问题对吧?就是我刚才说的:我凭啥要学它啊?给我个理由!它好在哪?

好在哪其实很简单:好在好用。

具体来说主要是两点:

  • 它的声明式 UI
  • 去掉了 xml,只使用 Kotlin 一种语言
  • 另外它还有别的好处,比如 Compose 是独立于操作系统的,所以方便调试。

但它最核心的优势就是上面这两个:声明式 UI 和去掉了 xml。我现在就对这两点展开解释一下。

声明式 UI

首先来说「声明式 UI」。从 Compose 诞生以来,声明式 UI 就一直是它最被大家称赞的点,说相比起传统写法的「命令式 UI」,「声明式 UI」怎么怎么好。但是关于什么是声明式 UI 以及传统的 UI 写法为什么叫命令式 UI,很多人却表示看不懂。看文章、看视频,都看不懂。所以今天我的重点之一就是给大家讲一下,到底什么是声明式 UI。

首先我们再来近距离对比一下传统的 xml 代码和 Compose 的代码分别是怎么写的。先来看一个 xml 代码:

这是一个 LinearLayout,里面有一个 TextView 和一个 Button 是吧?那么这个界面如果用 Compose 写,应该是怎么写呢?是这样的:

哎,还挺像的!不错,Compose 看起来没那么难学啊。等会儿!什么难学不难学,我们在讲什么?在讲什么是声明式 UI 对吧?这两段代码看起来完全没区别啊,怎么就一个是命令式一个是声明式了?

其实,所谓命令式 UI 和声明式 UI,和语言是无关的,也和写法是无关的。换句话说,一段 UI 代码摆在你面前的时候,你是不一定有办法判断出这是声明式还是命令式的。就比如这段 Compose 的代码——Compose 是声明式的对吧?但这段代码不能就直接被认定为是「声明式」的。

那到底什么是「声明式」呢?所谓声明式 UI,指的就是你只需要把界面给「声明」出来,而不需要手动更新

关键就在于这个「不需要手动更新」。

你看 xml 和 Compose 这两块代码,是不是都是把界面给声明出来的?是吧?是的啊,不要怀疑。但是当数据发生改变,需要跟着更新界面的时候,就不一样了。比如我现在要更新这个传统写法里 TextView 里面的值,我怎么写?
findViewById(),然后 setText() 是吧?而 Compose 呢?我们用 Compose 怎么处理界面的更新?

我们——不处理。我们什么都不做,界面会自动更新。听我这么说你可能会有点懵:什么?这不可能啊,界面都不知道它需要跟什么数据做联动,它怎么可能知道自己应该怎么更新?界面不知道,但我们可以告诉它啊!比如你这么写代码,你把文字的内容存到一个变量里:然后把这个变量作为参数放进 Text() 函数:

那么当 text 的值改变的时候,Text() 里的显示会自动跟着改变。Compose 里的界面元素会对依赖的数据自动进行订阅,这样数据改变了,界面马上就跟着变。这样的话,你只需要声明界面的规则,而并不需要具体去操作界面。当数据改变的时候,界面会自动改变,你不需要手动更新。这,就是声明式 UI。

而和它相对的,传统写法叫做「命令式 UI」,其实并不是指的 xml 这种写法,而是指的使用 Java 或者 Kotlin 代码来手动地更新界面,这种「手动更新」,或者说你具体指派、具体命令界面的每一步应该怎么更新,这就叫「命令式 UI」。

所以你看,声明式 UI 和语言相关吗?和写法相关吗?都是不相关的,对吧?所谓的声明式 UI,其实是从框架角度,描述的一个框架特性:全自动的对数据的变化进行联动的界面更新。

同样的,「声明式 UI」和「命令式」UI 比起来为什么好?因为你省事了,因为界面更新这个工作,本来是要我们自己做的,现在全由框架来帮我们做了。这个对于开发是非常有帮助的。多有帮助?大家都听说过数据绑定这种编程模式吧?

Android 官方也推出了这么一个 Data Binding 库,就专门是帮我们做这件事的:你把某个数据和某个界面控件用 Data Binding 关联起来,这样当这个数据改变的时候,界面控件的显示就也跟着改变。Android 官方专门出了这么个库,你说声明式 UI 有用不?非常有用对吧?

哎?不对啊?又不对了。我用 Data Binding 就能实现声明式 UI 了,还要 Compose 干嘛?是吧?

不是的。为什么不是?因为 Data Binding (不管是这个库还是这种编程模式)并不能做到「声明式 UI」,或者说,声明式 UI 是一种比数据绑定更强的数据绑定。比如在 Compose 里,你除了简单地绑定字符串的值,还可以用布尔类型的数据来控制界面元素是否存在。例如你再创建另外一个布尔类型的变量,用它来控制你的某个文字的显示:

注意,当 show 先是 true 然后又变成 false 的时候,不是设置了一个 setVisibility(GONE) 这样的做法,而是直接上面的 Text() 在界面中消失了。每次数据改变所导致的界面更新,看起来就跟界面关闭又重启、并用新的数据重新初始化了一遍一样。这,才叫声明式 UI,这是数据绑定做不到的。而且 Compose 并不是真的把界面重启了,它只会刷新那些需要刷新的部分,这样的话就能保证,它自动的更新界面跟我们手动更新一样高效。比如上面这里 show 的值从 true 变成 false 了,if 里面的 Text() 会被重新调用一次,但是外面的 Column() 和里面的 Text() 却不会被重新调用。很神奇吧?怎么做到的?靠的 code>@Composable 关键字,或者说靠的 Compose 的编译器插件,这个插件通过对编译过程的干预,把代码的逻辑拆到了我们看不到的细粒度,让这种看起来是连续的代码可以做到互相独立地执行。

这就是所谓的「声明式 UI」:

  • 传统的 UI 编写方式是,我们不可避免地要对界面进行直接修改,例如用 xml 来对界面进行「声明式」的设置,但在之后需要在 Java 或者 Kotlin 代码里对界面进行「命令式」的更新,例如控件的显示和隐藏、背景色的改变等等。这种「声明」加「命令」的写法,就叫「命令式 UI」。好吧我们可以称它为「混合式 UI」,但这没有实际的指导性意义,因为就像前面说的,「命令式」的叫法是为了针对像 Compose 这种「声明式」,作为区分而存在的。不管它是「纯命令式」还是「声明式初始化 + 命令式更新」,它的核心在于程序员需要指定更新的具体逻辑,这就是开发成本。
  • 而 Compose 是声明式的,我们只需要说明界面的规则,然后在依赖的数据改变的时候,Compose 会自动帮我们更新。
  • 那你说,Compose 的这种声明式 UI 爽不爽?很爽吧?
  • 另外我专门讲一下:

传统 UI 能做到的事,Compose 都是可以做到的:比如自定义绘制、自定义布局、自定义触摸,还有动画,Compose 都是可以做到的。动画也是声明式的:你让界面依赖某个数据,然后对这个数据做渐变,界面就发生动画了。有的人可能会担心 Compose 会不会因为这种「声明式 UI」设计太过于放飞自我而导致功能受到限制,某些边界情况还得依赖传统的 View。这种担心是多余的,不存在这种情况。Compose 的功能是不受限的,什么都能做。

第二点好处:去掉了 xml

这是 Compose 的第一个好处:声明式 UI。然后说一下它的第二个好处:去掉了 xml。这算什么好处啊是吧?现在就说一下。传统的 UI 写法是 xml + Java(Kotlin):

xml 负责界面框架的设置,Java(Kotlin)负责细节的调整;另外如果考虑到自定义 View,Java(Kotlin)也负责一部分的界面框架设置。但无论如何,App 要想快速开发,界面是需要 xml 和 Java(Kotlin)都用的。而都用就不可避免地涉及到两种语言的交互。

这种交互一般来说对于每个界面只有一次,就是把布局从 xml 文件读到 Java 里,然后就可以去布局和绘制了。但是这种交互虽然只有一次,但交互往往并不是 Java 和 xml 唯一的牵扯,因为 Java 还需要对布局里的控件进行操作,比如要用 findViewById() 来拿到 xml 里定义的控件。两种语言互相牵扯,就会带来很多局限性。

比如 ViewBinding 库为什么被推出?因为它解决了 findViewById() 的问题,比如空安全问题,对吧?如果没有问题,会需要解决吗?对吧?而 findViewById() 的问题就是因为界面需要 xml 和 Java(Kotiln) 一起写所导致的。

  • 而如果你试过用纯代码(Java 或者 Kotlin)来代替 xml 文件写布局,你就会体会到,在不需要双语言交互的情况下,代码会自由和方便得多。
  • 而 Jetpack Compose 就是纯 Kotlin 的,不用 xml。
  • 不过……我为啥不直接用上面说的「纯代码写布局」,而要去学Compose?

我不用 Compose 也可以抛弃 xml 啊。

  • 你如果真的试过,应该就不会这么说了。
  • 首先,我们想想为什么当初 Android 官方会推出 xml + Java 的写法?
  • 因为这种写法简单啊!xml + Java 这种双语言的写法只是有局限性,但在简单直观这个角度,它是完胜纯 Java 的写法的。
  • 而 Compose 不仅是只用了 Kotlin 这一种代码,还跟 xml 布局文件一样有写法简单的优点。——另外说实话,Compose 作为一个 2019 年诞生、2021 年正式版的全新 UI 框架,它的写法从目前看来是比 xml 还要方便得多的。

所以这就是 Compose 的另一个好处:它在抛弃了 xml 的同时还保持了写法的简单,完全解除了混合写法所带来的局限性,这也是它的一大优点。去掉一种语言而已,算是好处吗?太算了,这是一件非常棒的事。。

那么基于这两点好处:声明式 UI、抛弃 xml 变成单一语言的写法,Compose 很明显是要比传统的 UI 写法要更好的。这个所谓的「好」,解释开的话就是:写界面更简单更快速,并且我们能写出更复杂的界面。

Compose 的未来?

再说一个小话题:Compose 的未来如何?或者说,谷歌会弃坑吗?(让人害怕)

Android 团队和 JetBrains 团队现在在 Compose 上面花的精力非常大,而且就目前的情形来看,Compose 已经基本成型了。所以不可能被弃坑的。

另外一个关于未来的问题是:大厂会用它吗?我认为会,但我十分怀疑大厂的边缘团队可能不会太早用它。具体的我也说不准,我们拭目以待吧。

快教我!

那么我讲到这里,大家应该明白 Compose 是什么了。也到了我要真正讲 Compose 的硬核代码的时候了。

不过……演讲的时间比较有限,我只能把最重要的事情讲了,也就是刚才我说的:Compose 的本质、Compose 的扒皮,给大家讲讲 Compose 是什么、大概怎么用、好在哪、为什么好。至于更多的内容,大家如果想了解,可以在等我一下。我最近也正在准备更多的 Compose 的技术分享,等我准备好了,会以视频或者视频加文章的形式发布出来。

所以如果你有兴趣,欢迎在微信、 B 站、掘金、知乎搜索「扔物线」关注我,我很快就会更新的。

那欧了,以上就是我这次技术分享的文字形式的整理。如果你需要视频回放,可以去扫上面出现过的二维码找我的小助理要回放。另外如果你对 Jetpack Compose 或者其他 Android 开发的内容感兴趣,一定记得关注「扔物线」,更多的高质量视频分享很快就到,再等我准备一下下。