Android Jetpack Compose是谷歌推出的一种新的搭建UI的方式,使用Kotlin DSL的形式来组合UI组件。目前还在alpha阶段。苹果早也推出了类似的Swift UI,都是模仿前端的实现方式,目的都是UI更加轻量化和方便数据驱动。
Compose使用感受
Compose的使用比较简单,官方有连续的课程,还有比较完善的samples可以参考,网络上也能找到好多相关的教程,就不再重复写了。需要注意的是,Compose还在alpha阶段,API还未稳定,网上的教程好多是针对旧的API的,遇到问题还是尽量查看官方文档。下面就简单说说在使用Compose时的一些感受。
好的地方:
- 相对于之前的xml方式,代码编写起来可能更加熟练,对开发可能更亲切(个人感受)
- 一般的UI组件使用起来更加简单,如Text:
Text(
"text",
style = MaterialTheme.typography.body,
modifier = Modifier.padding(start = 4.dp)
)
相对于xml中声明TextView,代码量会小很多。
- 可能是由于目前Compose提供的组件比较少,布局实现比较单一,感觉实现比较复杂的布局比XML高效,简洁很多。而且性能也比XML高,毕竟少了xml解析的过程。
- 可以多个UI组件一起预览。传统方式一次只能预览一个xml布局,使用Compose之后,只要在Compose上添加@preview注解,在一个文件内都可以预览。在搭建UI时几乎可以不再需要安装到真机和模拟器上进行检查。
- 数据驱动UI更新更加直观和高效,只需要声明state即可,数据改变时会自动更新UI,不再需要之前的Livedata等比较复杂的机制。
- Theme更加强大和自由,以前在XML中声明Theme和自定义主题中某个组件的样式,是比较麻烦的,因为属性太多了。现在就比较方便了可以直接使用Material Design主题中定义好的样式,更加规范高效,而且深色主题适配更加简单。
- 动画API更加简单了,不再需要复杂的写法,会自动根据之前的属性生成对应的动画,如更改组件的大小,只需要在modifier中添加:
modifier.animateContentSize(animSpec = TweenSpec(300)),
就可以了,系统会自动监控这个组件的大小的变化,生成动画。
不好的地方
- 由于使用的是Kotlin DSL,所以代码排版缩进会比较多,相对于一般代码结构,直观性比较差。但是还是比Flutter的好一些。
- 由于要实现实时预览,每次修改Compose都需要编译,如果项目比较大,编译时间很长,那体验就会很差了
- 组件的丰富度还比较欠缺,需要进一步完善
- 官方的教程,文档包含的内容有限,有很多之前的组件找不到在Compose中对应的组件,命名也发生了变化。寻找起来比较麻烦。大部门只能看官方的文档。而且对复杂的界面布局的实现没有比较完善的指导文档。
- jetpack组件还在推广中,Compose已经和好多AndroidX的组件冲突了,如果Compose推进的比较快,那还有必要学习使用AndroidX中的UI和管理UI组件吗? 一般的项目也不会想两套共存吧? 所以如何推广Compose还是个问题, 好处是不像iOS的Swift UI那样,和系统版本绑定。
Compose的实现
最初看到Compose的时候,以为就是对之前的View组件做了一次封装,然后底层再做组装,渲染处理。后来看了下源码,发现不是这样的,是重新实现了一套。如Text的具体实现是CoreText
,CoreText
的layout实现:
Layout(
children = if (inlineComposables.isEmpty()) {
emptyContent()
} else {
{ InlineChildren(text, inlineComposables) }
},
modifier = modifier
.then(controller.modifiers)
.then(
if (selectionRegistrar != null) {
Modifier.longPressDragGestureFilter(
longPressDragObserver(
state = state,
selectionRegistrar = selectionRegistrar
)
)
} else {
Modifier
}
),
minIntrinsicWidthMeasureBlock = controller.minIntrinsicWidth,
minIntrinsicHeightMeasureBlock = controller.minIntrinsicHeight,
maxIntrinsicWidthMeasureBlock = controller.maxIntrinsicWidth,
maxIntrinsicHeightMeasureBlock = controller.maxIntrinsicHeight,
measureBlock = controller.measure
)
TextController
控制Text的layout、state、selection、measure和draw。底层其实都是通过TextDelegate实现的
fun layout(
constraints: Constraints,
layoutDirection: LayoutDirection,
prevResult: TextLayoutResult? = null,
respectMinConstraints: Boolean = false
): TextLayoutResult {
val minWidth = if (respectMinConstraints || style.textAlign == TextAlign.Justify) {
constraints.minWidth.toFloat()
} else {
0f
}
val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth.toFloat()
} else {
Float.POSITIVE_INFINITY
}
if (prevResult != null && prevResult.canReuse(
text, style, maxLines, softWrap, overflow, density, layoutDirection,
resourceLoader, constraints
)
) {
return with(prevResult) {
copy(
layoutInput = layoutInput.copy(
style = style,
constraints = constraints
),
size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
)
}
}
val multiParagraph = layoutText(
minWidth,
maxWidth,
layoutDirection
)
val size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
return TextLayoutResult(
TextLayoutInput(
text,
style,
placeholders,
maxLines,
softWrap,
overflow,
density,
layoutDirection,
resourceLoader,
constraints
),
multiParagraph,
size
)
}
layout最终返回一个TextLayoutResult
,TextLayoutResult
在draw的时候使用:
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
TextPainter.paint(canvas, textLayoutResult)
}
最终调用了TextPainter
,TextPainter
的paint方法:
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
val needClipping = textLayoutResult.hasVisualOverflow &&
textLayoutResult.layoutInput.overflow == TextOverflow.Clip
if (needClipping) {
val width = textLayoutResult.size.width.toFloat()
val height = textLayoutResult.size.height.toFloat()
val bounds = Rect(Offset.Zero, Size(width, height))
canvas.save()
canvas.clipRect(bounds)
}
try {
textLayoutResult.multiParagraph.paint(
canvas,
textLayoutResult.layoutInput.style.color,
textLayoutResult.layoutInput.style.shadow,
textLayoutResult.layoutInput.style.textDecoration
)
} finally {
if (needClipping) {
canvas.restore()
}
}
}
设计思想还是View的那套,但是所有组件的实现更加扁平了。不再有View或Viewgroup的继承关系,大部分组件都是直接自己直接实现layout,measure和draw。所以看起来更加简洁。
State
Compose的更新数据显示是通过State来实现的,而且比之前的LiveData更加简单,如:
数据:
val list = listOf(
"ListItem1“,
"ListItem2“,
"ListItem3“,
"ListItem4“,
"ListItem5“
)
val data by remember{ mutableStateOf(list) }
UI:
LazyColumnFor(
items = data,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp))
{ text ->
Text(text = text)
}
添加数据:
data.value = list += listOf("ListItem6")
添加之后列表会自动刷新数据和UI。不需要跟之前一样去主动notify UI更新。
其中remember{}
的表达的意思是跟字面意思一致,就是记住后面block中产生的value, 只有在UI组合时才会产生值。
mutableStateOf
产生一个SnapshotMutableState
,里面的value的读和写都是被监控的。在UI组合完成之后Composer会一直监控state, 如果有值发生变化则会触发Recomposition, 进行UI更新。
Recomposition
官方翻译为重组,就是重新调用compose组合UI的过程,系统会根据需要使用新数据重新绘制函数发出的组件。因为UI是一直显示的,重组可能会很频繁,为了保证流畅性,官方做了很多优化,如并行处理,自动跳过不需要重组的组件和使用乐观算法优化重组。总之和之前的UI实现一样,不要在compose组合期间执行比较耗时的逻辑。