Compose 于2019年的Google IO大会首次发布,当时感觉前景并不乐观,想推翻已存在10年之久的现有视图体系谈何容易,更何况将来与Flutter等同门兄弟的关系又该如何相处?
没想到时隔仅两年,本届 IO 大会上就宣布 Compose 1.0 即将到来。其实从年初 Beta 版的一系列造势活动就能看出 Google 在 Compose 推广上的决心之大,只要官方发力编程语言都可以短期内切换(Java > Kotin),更何况一个UI框架?所以不必怀疑, Compose必将成为新的UI开发标准。
随着稳定版的到来,现在正是学习Compose的好时机。让我们借本次 GoogleIO 上的内容,了解一下 Compose 将为 Android 开发带来哪些变化。
1
为什么要用 Compose ?
Jetpack Compose is Android’s modern toolkit for building native UI.
这是官方对 Compose 的定义。通过与Android现有视图体系对比就能理解为什么 Compose 更加 “现代”。
现有的 Android 视图体系从 2010年 以来没有发生太大变化,10年间无论从硬件规格还是APP复杂度都发生了极大变化,这套已经跑了10年的技术体系也已经显得有些落伍。
近年来,React等声明式框架的出现改变了前端的开发方式, Android 借鉴了 React的先进思想为打造了一款用于Native开发的声明式UI框架:Jetpack Compose。使用 Compose 可以显著减少创建页面的时间,提高了UI开发效率。
2
声明式 VS 命令式
现行的 Andoird 视图体系属于传统的命令式开发方式,一般使用XML布局,而后通过 findViewById 获取控件的引用,命令式地更新状态、刷新UI。命令式的视图体系有以下特性:
-
UI是可变的:控件接受命令后通过变化自身刷新UI。
-
UI持有State:控件的变化正是通过改变自身状态实现的。
随着界面越来越复杂,控件越来越多,各控件 State 难以保持同步,UI显示不一致的Bug频发。我们的很多精力花费在了如何能准确且不遗漏地更新所有该更新的控件上。
声明式UI与命令式UI的特点截然相反,正好可以弥补命令式的缺陷:
-
UI不可变 : @Composable函数不返回任何可引用句柄,无法被外界改变。
-
UI不持有State: @Composable函数无法持有状态的,显示的数据都需要通过参数传入。
声明式UI以一个“纯函数”的方式运行,当 State 变化时函数重新执行刷新UI。
KeyPoint: Compose使用 @Composable 函数来构建UI可以更好地贯彻声明式UI的特点
3
基于 Kotin 的 DSL
声明式UI需要有一个与之匹配的 DSL 语言做支持,例如 React 中的 JSX。在 Android 全面拥抱 Kotlin 的今天,基于 Kotlin 的 DSL 几乎是唯一选项,庆幸的是 Kotlin 语法优势使得其DSL足够强大和好用。
KeyPoint: 使用 DSL 组装 UI 的过程其实就是对 @Composable 函数的定义过程。
@Composable
fun MessageList(messages: List<String>) {
Column {
if (message.size == 0) {
Text("No messages")
} else {
message.forEach { message ->
Text(text=messag)
}
}
}
}
上面例子中 MessageList 是一个展示消息列表的UI组件, 参数 message 即展示的数据。DSL 让我们可以很直观地书写多层嵌套UI ,例如在 MessageList 中嵌套 Column、Text 等。
基于高级语言的 DSL 是图灵完备的。我们在构建UI的同时,同步添加逻辑:当没有消息时显示 ”NO message“ 。这是 JSX 这类标记型的 DSL 所无法完成的。
当 message 发生变化时,MessageList 重新执行,这个过程叫重组(recomposition)。Composee 的 UI 正是通过不断重组来实现刷新。
4
高性能的重组
当数据变化时会触发重组,很多人担心大面积的重组是否会影响性能。
React 每次 render 会生成 VirtualDom,通过 diff算法精准更新 DOM 实现局部刷新,VirtualDom 相比 DOM 创建成本很低从而保证了 React 的性能。
Compose 也采用了类似的思想,通过 diff 实现局部刷新。不同的是,Compose 没有引入 VirtualDom 这样的树形结构,而是在 Gap Buffer 这样线性结构上进行 diff。但本质上是相同的,可以将 Gap Buffer 理解为一个树形结构经 DFS 处理后的数组,数组单元通过 key 标记其在树上的位置信息。
Compose 在编译期为 Composable 生成带有位置信息的 key,存入到 Gap Buffer 数组的对应位置。运行时可以根据 key 来识别 Composable 节点是否发生了位置变化,以决定是否参与重组。
同时,Gap Buffer 还会记录 Composable 对象关联的状态(State 或 Parameters),仅仅当关联状态变化时,Composable 才会参与重组,函数才会重新执行。
KeyPoint: Compose 编译器保证 Composable 尽可能跳过不必要的重组,有利于提高 Compose 的重绘性能
5
State 管理
Compose 的核心是 State 驱动 UI,UI 是 State变化后的产物。传统视图体系,State 只是 UI 控件的属性,UI一旦创建后会长期存在。
KeyPoint: 传统开发State从属于UI,Compose中UI依附于State
通过一个 Checkbox 的例子可以更好地理解 State 与 UI 的关系:
@Composable
fun MessageList(messages: List<String>) {
Column {
var selectAll by remember { mutableStateOf(false) }
Checkbox(
checked = selectAll,
onCheckChange = { checked ->
selectAll = checked
}
)
...
}
}
selectAll是 CheckBox 可以参照和修改的 state。勾选 Checkbox ,selectAll 发生变化,Checkbox 发生重组更新状态。
相对于 Checkbox 的重组,selectAll 通过 remember { } 可以跨越重组而持续存在。Composable 大量借鉴了 React Hooks 的设计,例如 remember{} 就参考了 useMemo()。
6
数据流单向流动
selectAll 状态改变,Checkbox 重组,数据永远是自上而下的单向流动。我们把范围放大,MessageList 的数据也是通过参数从上层传入的。
@Composable
fun ConversationScreen() {
val viewModel: ConversatioinViewModel = viewModel()
val message by viewModel.messages.observeAsState()
MessageLit(messages)
}
@Composable
fun MessageList(message: List<String>){
...
}
Compose 可以配合现有 Jetpack 组件的使用,例如 ViewModel、LiveData 等,对于一个标准的Jetpack MVVM项目,将很容易将 UI 部分替换成 Compose。
Composalbe 中调用 viewModel() 可以获取当前 Context 的 ViewModel, observeAsState() 将 LiveData 转换为 Compose State 并建立绑定。当 LiveData 变化时,ConversationScreen 会发生重组,内部的 MessageLit 、MessageItem 由于依赖了参数 messages,都会参与重组。
所有子 Composalbe 的数据都来自顶层的 ScreenState ,这就是所谓的唯一可信源(Single Source Of Truth)。我们只要关注 ScreeState,就可以推断出当前UI的状态,这种模式也很利于测试。
KeyPoint:Composable 的数据自上而下单向流动,所有的数据都应来自顶层的唯一可信源。
7
副作用与 Lifecycle
我们也可以不借助 ViewModel, 直接在 Composalbe 中操作数据。但是数据操作涉及 IO,不应该跟随重组反复进行,应该被当做**副作用(SideEffect)**处理。
@Composable
fun ConversationScreen() {
var message = remember { mutableStateOf(emptyList()) }
val scope = rememberCoroutineScope()
SideEffect {
scope.launch {
message = apiService.getMessage()
}
}
MessageLit(messages)
}
-
SideEffect {...} 处理副作用,只在 Composable 首次上树显示时执行一次,不会随重组反复执行;
-
rememberCoroutineScope() 可以获取与当前 Composalbe 关联的 CoroutieScope,当 Composable 从树上移除时,其中的协程会随之取消。
当副作用用到协程时,也可以直接使用LaunchedEffect,更加方便:
//LaunchedEffect中提供了CoroutineScope,可以直接启动协程
LaunchedEffect {
message = apiService.getMessage()
}
这里涉及到 Composalbe 生命周期的定义:
Enter:挂载到树上,首次显示,类比 React 的 Mounting。
Composition:重组刷新UI,类比 React 的 Updating。
Leave:即从树上移除,不再显示,类比 React 的 Unmonting。
Composalbe 引入生命周期,便于处理那些非纯函数的逻辑(不能跟随重组反复执行的逻辑),Compose 提供了 SideEffect{} 等函数来处理这些逻辑。
Composalbe 越接近纯函数越利于复用,所以SideEffect、LaunchedEffect等副作用函数越少越好,推荐尽量移动到 ViewModel 中处理。
8
功能完备的UI系统
KeyPoint:Compose目前的 UI系统功能完备,可以完全覆盖Android现有视图系统的所有能力。
各种UI组件
所有常见的UI组件在 Compose 中都能找到对应实现,甚至Card、Fab、AppBar等 Material Designe 的控件也一应俱全、开箱即用 。
Card {
Text("Card Content")
}
FloatingActionButton() {
Icon(Icons.Filled.Favorite)
}
TopAppBar(
...
)
列表List
Compose 的列表非常简单,无需再写烦人的 Adapter。
@Composable
fun MessageList(list: List<Message>) {
Column {
...
LazyList { // this :LazyListScope
items(list) { item ->
when(item.type) {
Unread -> UnreadItem(message)
Readed -> ReadedItem(message)
}
}
}
...
}
}
LazyList 确保了列表条目的懒加载,items{...} 中创建每条项目的 Composalbe,MultiType 直接用 when 语句处理,简单高效。
布局Layout
Compose 提供了多种容器类Composalbe,可以对子组件进行布局,简单易用且功能强大。
Row {
// ≈ Horizontal LinearLayout
}
Column {
// ≈ Vertical LinearLayout
}
Box {
// ≈ FragmeLayout
}
与现有视图体系不同,Compose的布局不允许测量多遍,即使布局多层嵌套也不会有性能问题。
我们也可以方便地进行自定义布局,通过简单的函数调用完成 measure 和 layout 过程。
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// Measure the composables
val placeables = measurable.measure(constraints)
// Layout the comopsables
layout(width, height) {
placeables.forEach { placeable ->
placeable.place(x, y)
}
}
}
Modifier操作符
Compose 通过 通过一系列链式调用的Modifier操作符来装饰 Composable 的外观。操作符种类繁多,例如size、backgrounds、padding的设置以及 click事件的添加等。
Modifier修饰前 | Modifier修饰后 |
---|---|
![]() | ![]() |
@Composable
fun FollowBtn(modifier: Modifier) {
Text(
text = "Follow",
style = typography.body1.copy(color = Color.White),
textAlign = TextAlign.Center,
modifier = modifier //在外部装饰的基础上进一步添加装饰
.clickable(onClick = {...})
.shadow(3.dp, shape = backgroundShape)
.clip(RoundedCornerShape(4.dp))
.background(
brush = Brush.verticalGradient(
colors = listOf( Red500, orange700),
startY = 0f,
endY = 80f
)
)
.padding(6.dp)
)
}
Modifier 的链式调用便于在外层装饰的基础上进一步装饰,同时Modifier 集中配置的方式也有利于对在多个 Composalbe 之间复用。
动画Animatioin
Compose 动画也是基于 State 驱动不断重组完成的。
@Composable
fun AnimateAsStateDemo() {
var isHighLight by remember { mutableStateOf(false) }
val color by animateColorAsState (
if (isHighLight) Red else Blue,
)
val size by animateDpAsState (
if (isHighLight) LargeSize else SizeSize,
)
Box(
Modifier
.size(size)
.background(color)
)
}
isHighLight 状态变化时,Color 和 Size 以动画的形式进行过度。
主题Theme
Compose 的 Theme 摆脱了对 XML 的使用。Theme 一般会作为最顶层的 Composalbe 出现,所有内部 Composalbe 都会应用当前主题的配置。
主题切换也变得非常容易。例如根据系统Dark/Light主题,为自定义主题设置不同颜色:
@Composalbe
fun YellowTheme(
content: @Composalbe () -> Unit
) {
val colors = if (isSystemInDarkTheme()) {
YellowThemeDark
} else {
YellowThemeLight
}
MaterialTheme(colors, conent)
}
APP中应用自定义主题:
@Composalbe
fun TopicScreen() {
YellowTheme {
Scaffold(
backgroudndColor = MaterialTheme.colors.surface
) {
TopicList()
}
}
}
Light | Dark |
---|---|
![]() | ![]() |
9
开发中实时预览
现状基于 XML 的预览效果很鸡肋,导致很多开发者都习惯于实机运行查看UI。
KeyPoint: Compose 预览机制可以做到与真机无异,真正的所见所即得。
当我们相对对下面的 Composable 进行预览时。
@Composable
fun Greeting(name: String) {
Text (text = "Hello $name!")
}
只需创建一个无参的 Composalbe,并添加 @Preview 注解即可。
@Preview
@Composable
fun PreviewGreeting() {
Greeting("Android")
}
@Preview 的 Composalbe 不会编译进最终产物,对包大小没有影响。而且 @Preview 可以作为入口直接运行在设备上查看,也正因如此,不能为它的签名添加任何参数。
10
良好的互操作性
KeyPoint: Compose 能够与现有 View 体系能一起使用,你可以为一个既有项目引入 Compose,并逐渐切换。
有一些功能性的控件目前仍然需要原生 View 的支持,比如 WebView、MapView 等,通过良好的互操作性可以为 Compose 获得更多原生支持。
@Composable
fun WebView() {
val url = remember { mutableStateOf(...) }
// 添加Adds view to Compose
AndroidView(
modifier = Modifier.fillMaxSize()
factory = { context ->
// 创建原生的WebView供Compose使用
WebView(context).apply {
...
}
},
update = { view ->
// 当Satte变化时,可以在这里更新原生的CustomView
view.loadUrl(url)
}
)
}
11
总结 & 展望
不可否认,Compose 与现行的UI开发方式存在较大差异,学习曲线相对陡峭。本文针对 Compose 特性的介绍都是点到为止,主要目的是帮大家在展开系统学习之前在心智上做一个铺垫。
最后 回顾一下前文中梳理的 KeyPoint:
Compose特性 | KeyPoint |
---|---|
声明式框架 | Compose 使用 @Composable 函数构建UI,贯彻了声明式UI的特点 |
基于Kotlin的DSL | DSL 组装 UI 的过程其实就是对 @Composable 函数的定义过程 |
高性能的重组 | 编译器确保 Composable 尽可能跳过不必要的重组,有利于提高重绘性能 |
状态管理 | UI 依赖于 State,State变化驱动UI刷新 |
单向数据流 | Composable的数据自上而下单向流动,所有的数据都应来自顶层的唯一可信源 |
副作用处理 | Compose的生命周期机制可以处理副作用 |
UI系统 | Compose目前的UI功能完备,可以完全覆盖Android现有视图系统的所有能力 |
实时预览 | Compose 预览机制可以做到与真机无异,真正的所见所即得 |
互操作性 | Compose 能够与现有 View 体系并存,你可以为一个既有项目引入 Compose |
未来的 Compose
今年7月 Compose 1.0 就会正式与大家见面,不久正式版 AS 也将支持 Compose 开发。
现在已经有越来越多的 Jetpack 官方库以及常用的三方库开始加入对 Compose 的支持,随着其生态越发完善,相信不用多久就能够见到 Compose 在真正的项目中落地。
Compose-Desktop、Compose-Web 等项目的发布将为 Compose 提供跨平台的能力,作为 Android 开发的你未来可以通过 Compose 扩大你的开发领域、甚至延长你的职业生涯。
错过 Flutter 的你,不要再错过 Compose 了。Happy Composing!
官方学习资料
https://goo.gle/compose-pathway
https://goo.gle/compose-samples
https://goo.gle/compose-docs
中文学习项目
https://docs.compose.net.cn/