Jetpack Compose 是一个独立的 UI 工具包,它结合了响应式编程模型和 Kotlin 编程语言的简洁性和易用性,旨在简化 UI 开发。
它是完全声明性的,意味着可以通过调用一系列将数据转换为UI的函数来描述UI。当基础数据更改时,框架会自动调用这些函数,从而更新视图层次结构。
现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强调非常有可能产生变化且无法用于生产环境。不过简单了解下 Compose 还是不错的。
1. 准备
要启动新的Compose项目,请打开Android Studio 4.0,然后选择启动新的Android Studio项目:
创建新项目时,从可用模板中选择“
Empty Compose Activity”,注意
minimumSdkVersion 至少为21及以上,“Language” 必须为kotlin:
2. Jetpack Compose构建UI的特点
Button 继承自 TextView,理论上我们只需要一个文本 + 可点击的区域就可以了,但是由于 TextView 的特性,它本身是可以长按出现复制、选择功能的,但是一个 Button 要这些功能有什么用呢?Jetpack Compose 的核心: 组合优于继承,所有的 UI 都是通过组合实现,不存在继承关系。
目前的 UI 构建方式来说,写一个自定义 View 需要实现测量和布局,响应用户的行为需要实现大量的 Listener 事件,同时还要配合 XML 自定义属性,非常繁琐。而且以目前的View代码量体积来说,想要完全优化重构是不现实的。发布一个全新的 UI 构建库,从根本上解决问题,所以 Google 推出了全新的 Android UI 组件库 Jetpack Compose。
Jetpack Compose 试图改变原有的 UI 构建方式,同时带来以下 4 点全新的改变:
- UI 的变化更新不再跟随 Android 大版本的发布而更新
- 编写 UI 代码不需要掌握庞大繁琐的技术栈
- 简单直接的状态控制以及用户行为处理
- 使用更少的代码来编写 UI
说了这么多,用一下看看吧。
3. 使用Compose构建UI
新创建好的MainActivity
长这样:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview
@Composable
fun DefaultPreview() {
MaterialTheme {
Greeting2("Android")
}
}
使用setContent
用来定义布局,但不是使用XML文件,而是在其中调用Composable
函数。要创建可组合函数,只需将@Composable
注释添加到函数。该函数可以调用其他的@Composable
函数。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!") //Text是library提供的可组合函数。
}
可组合函数是带有@Composable注释标记的Kotlin函数
@Preview("Text preview")
@Composable
fun DefaultPreview() {
Greeting(name = "Android")
}
用@Preview
标记任何一个无参数的Composable
函数并Build项目,就可以在Android Studio中看到预览。
遵循单一职责原则。@Composable
函数负责单个功能,该功能完全由该函数封装。例如,如果要为某些组件设置背景色,则必须使用Surface
可组合功能。
给Text
设置背景色,我们需要定义一个Surface
包裹它。
@Composable
fun Greeting(name: String) {
Surface(color = Color.Yellow) {
Text (text = "Hello $name!")
}
}
Modifiers
Modifiers
是为UI组件提供其他修饰的属性列表。目前可用的修饰符有:Spacing
,AspectRatio
和修改Flexible Layouts
布局的Row
和Column
。
@Composable
fun Greeting(name: String) {
Surface(color = Color.Yellow) {
//Spacing 为文本添加填充
Text(text = "Hello $name!", modifier = Spacing(24.dp))
}
}
点击Build & Refresh
按钮查看预览:
请注意,@Composable注释仅对创建UI的函数是必需的。它可以调用常规函数和其他Composables函数。如果某个功能不满足这些要求,则不应使用@Composable注解。
创建通用Container
@Composable
fun MyApp(child: @Composable() () -> Unit) {
MaterialTheme {
Surface(color = Color.Yellow) {
child()
}
}
}
该函数以Composable
函数(在此称为)的 lambda 作为参数,该 lambda child
返回Unit
。我们返回Unit
是因为所有Composable
函数都必须返回Unit
。
在
@Composable()
将 Composable 函数用作参数时,需要添加注解:
fun MyApp(child: @Composable() () -> Unit) { ... }
后面代码就可以如此调用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Greeting("Android")
}
}
}
}
@Preview("Text preview")
@Composable
fun DefaultPreview() {
MyApp {
Greeting("Android")
}
}
将UI组件提取到Composable函数中,以便我们可以重复使用它们而无需复制代码。比如使用不同的参数重用同一Composable函数。以垂直顺序排列,我们使用ColumnComposable函数(类似于垂直LinearLayout)。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent()
}
}
}
}
@Composable
fun MyScreenContent() {
Column {
Greeting("Android")
Divider(color = Color.Black)
Greeting("there")
}
}
@Composable
fun Greeting(name: String) {
Text (text = "Hello $name!", modifier = Spacing(24.dp))
}
@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
Divider
是提供的可组合函数,用于创建水平分隔线。
可以像Kotlin中的任何其他函数一样调用compose函数。可以添加语句来影响UI的显示方式,构建UI非常方便。
@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
Column {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
不知道你有没有这种想法,这里的for循环会不会就是把 Text 翻译为 TextView,然后此方法就是接收一个 List<String> 对象,返回一个 List<TextView>?
显示布局边界看下:
事实并非如此,它所有的可绘制元素都不是 Android 原生的 View,其顶层View为AndroidComposeView,内部在维护的 ComponentNode负责绘制。
数据流
通过将对象作为参数传递给Composable函数,数据向下流动。
@Composable
fun MyExampleFunction(items: List<Item>) {
Column {
for (item in items) {
RenderItem(item = item)
}
}
}
@Composable
fun RenderItem(item: Item) {
Row {
Text(text = item.name)
WidthSpacer(4.dp)
Text(text = item.description)
}
}
RenderItem从调用Composable函数接收其所需的数据作为参数。如果我们要处理Item单击,则使用lambda 将信息从层次结构的底部传递到顶部。
@Composable
fun MyExampleFunction(items: List<Item>, onSelected: (Item) -> Unit) {
Column {
for (item in items) {
RenderItem(item = item, onClick = { onSelected(item) })
}
}
}
@Composable
fun RenderItem(item: Item, onClick: () -> Unit) {
Clickable(onClick = onClick) {
Row {
Text(text = item.name)
WidthSpacer(4.dp)
Text(text = item.description)
}
}
}
数据随参数向下流动,事件随lambda向上流动。
使用@Model管理状态
对状态更改做出反应是Compose的核心。如果数据发生更改,则可以使用新数据调用Composable函数将数据转换为UI,从而更新UI。
Compose使用自定义的Kotlin编译器插件,当基础数据发生更改时,可以重新调用函数以更新UI视图。
Compose提供了@Model注解,该注解可以放在任何类上。如果数据发生更改,从@Model参数读取值的可组合函数将自动被调用。该@Model注解将导致编译器重写类,使它可观察和线程安全。可组合函数将自动订阅它读取的类的任何可变变量。如果它们发生变化,将重新组合读取这些字段。
举个栗子,比如做一个计数器,跟踪用户单击多少次Button
:
@Model
class CounterState(var count: Int = 0)
在CounterState加上注解@Model,任何将此类作为参数的Composable函数在count值更改时将自动重新组合。定义Counter为一个Composable函数,该函数采用CounterState一个参数,并发出Button,显示单击了多少次。
@Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
}
)
}
每次count更改时,Button都会重新构成并显示的新值count。
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
Divider(color = Color.Transparent, height = 32.dp)
Counter(counterState)
}
}
感觉有种JS上Object.setProperty
的即时感,确实如官方所说, Jetpack Compose 受到了 React、Litho、Vue、Flutter 的启发。
布局
与屏幕中心对齐,我们可以使用列的crossAxisAlignment参数:
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column(crossAxisAlignment = CrossAxisAlignment.Center) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
Divider(color = Color.Transparent, height = 32.dp)
Counter(counterState)
}
}
@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
刷新预览:
@Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
},
style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
)
}
Compose 对 ConstraintLayout 的支持还正在进行中。对于现有的布局控件,以后应该都是会添加支持的。
Compose 提供了 VerticalScroller 和 HorizontalScroller 来生成列表,在使用上与 RecyclerView 是完全不同的体验:
@Composable
fun MyApp() {
VerticalScroller {
Column {
repeat(20) {
Row(mainAxisSize = LayoutSize.Expand) {
Container(height = 48.dp) {
Text("Item $it", modifier = Spacing(left = 16.dp))
}
}
}
}
}
}
事实上,Scroller 与 ScrollView 更接近,只提供了一个滚动的功能,并没提到有对 View 进行回收复用。
兼容现有UI的构建方式
上图:
使用 Jetpack Compose 编写的 View,可以无缝的通过 xml 在原有视图上使用,只需要增加一个 @GenerateView 注解。
原有的 View 也支持 Jetpack Compose 写法。目前在预览版里@GenerateView
注解还无法使用,不免有些遗憾~
自定义view
@Preview
@Composable
fun errorView() {
val checkBox = @Composable {
Draw { canvas: Canvas, parentSize: PxSize ->
val size = parentSize.width.value
val outer = RRect(0f,0f,size,size).withRadius(Radius(10f, 10f))
canvas.drawRRect(outer, Paint().apply {
color = Color.Red
})
}
Draw { canvas: Canvas, parentSize: PxSize ->
val paint = Paint().apply {
color = Color.White
strokeCap = StrokeCap.round
strokeWidth = 10f
isAntiAlias = true
}
val size = parentSize.width.value
val leftStart = Offset(size / 4, size / 4)
val leftEnd = Offset(size / 4 * 3, size / 4 * 3)
val rightStart = Offset(size / 4 * 3, size / 4)
val rightEnd = Offset(size / 4, size / 4 * 3)
canvas.drawLine(leftStart, leftEnd, paint = paint)
canvas.drawLine(rightStart, rightEnd, paint = paint)
}
}
Layout(children = checkBox) { _, _->
layout(IntPx(200), IntPx(200)){}
}
}
Jetpack Compose 中实现自定义 View 的过程也非常简单,我们只需要关注 Draw 和 Layout 这两个方法就好了,绘制过程和之前一样,还是经过 measure、layout、draw ,但写法很精简。
Jetpack Compose带给我们一种Android新的构建UI方式的实践,从语法上来看还是有些Flutter的影子,声明式UI和数据驱动带给我们更多想象力。在未来的计划中,Jetpack Compose 会支持 Kotlin 协程、会支持现有的 Android Arch Componet 、会有更完善的动画机制。一起期待吧~