谷歌开发者大会扔物线演讲原稿整理: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 开发的内容感兴趣,一定记得关注「扔物线」,更多的高质量视频分享很快就到,再等我准备一下下。