Compose 出来也有一段时间了,刚 1.0.0 发布的时候简单看过一些,却一直没有尝试。最近 GDG 突然举办了一个 Compose 学习挑战赛,还有奖品,初级回答几个问题就可以拿到周边,进阶挑战赛还有机会得到一本 Compose 的书,便参加了这场学习挑战赛,而且最近写 Java 实在多,正好写一写 Kotlin 转换一下心情。
Compose是什么
“Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。”这是来自官方的描述,总之就是一个 Android Framework 扩展包,用一种新的方式来写 Native UI。它看上去,就长这样:
是不是感觉非常眼熟?这 Scaffold,这 AppBar,还有 Column,这不就是 Flutter 的 API 么?!看着这个代码就非常地好理解了,如果你熟悉 Kotlin,又写过 Flutter 或者 React,Compose 基本上就是 0 成本上手。这段代码,对应到 Android XML 差不多就是长这样:
<Scaffold android:layout_width="match_parent" android:layout_height="match_parent"> <TopAppBar android:title="MyApp Title"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="match_parent" android:layout_height="match_content" android:onClick="handleClick" android:text="Ciallo~"/> </LinearLayout> </Scaffold>
如何开发一个页面
简单介绍下 Compose 里一些基本的要素,有了这些,就可以写一个简单的页面了!
▐ 写一个 Composable 组件
在 Compose 中,所有的组件,都是一个函数,这个函数使用 @Composable 注解标识。只有使用这个注解标识的函数才会被认为一个 Compose 组件,所有 Compose 组件也只能在 @Composable 标识的函数中使用。下面这段代码就是自定义了两个 Compose 组件。
与 Flutter / React 一样,Compose 中也万物皆组件,一个组件可以是一个页面,也可以被其它组件使用。
/** * 定义了一个 HomePage 组件 * 该组件使用了一个官方的 Box 容器,以及自定义的 Body 组件 */ @Composable fun HomePage() { Box { Body(title = "HomePage") { Text(text = "content") } } } /** * 定义了一个 Body 组件 * 组件本身可以传递参数,函数参数即为组件的参数 * 参数也可以是一个 Composable 组件,因此可以向组件传递另一个组件 * 从而达到类似 Flutter / React children 的效果 */ @Composable fun Body(title: String, content: @Composable () -> Unit) { Column { Text(text = title) content() } }
Compose 的 DSL 里大量应用了 Kotlin 的语法特性,最基本的就是将函数当作参数传递以及尾闭包。在 Kotlin 中,若函数的最后一个参数是也是一个函数,则这个参数传递时可以写在函数调用后面,于是就看起来像是函数调用后面加了一对大括号。
▐ 关联 Activity
组件写好了,接下来就可以将它显示出来了。Compose 仍然依赖于 Android Framework 的 Activity 体系,本文章中不探究 Compose 组件是如何渲染的,这里可以简单的认为,所有的 Compose 组件其实只是 Kotlin DSL 的产物,它仍然在 View 体系中渲染(实际上这并不准确)。因此,要把 Compose 组件像 View 一样,通过 setContent 方法挂载到 Activity 上。
/** * 必须继承 ComponentActivity */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 调用 setContent 函数 setContent { HomePage() } } }
到这里为止,其实就可以将 App 运行起来了。
▐ 逻辑 & 状态
到目前为止,我们写出来的页面只是一个静态页面,但如果要处理业务逻辑,改变 UI 的样式该怎么办?原生 Android 通过 findViewById 拿到 View,然后设置 View 中的属性,内部调用 requestLayout 触发下一帧的重绘,那 Compose 该怎么做?
Compose 是一个声明式、响应式的 UI 框架。不像原生 Android 需要通过命令式的代码调用,显示地去改变某个 View 的状态。在 Compose 中,只需要使用“状态”,将状态与组件的属性绑定,在触发逻辑时,改变状态值即可触发 UI 刷新。这一点,其实原生 Android View 体系中也可以做到了,有兴趣的同学可以去看下 Android ViewModel + dataBinding 写 UI 的方式。本质上它其实是帮我们向 ViewModel 中的 LiveData 注册了观察者,属性变化时,调用了 View.setXXX 方法。
Android dataBinding XML 和 ViewModel 示例,来自我的 GitHub 仓库中三年前的项目。
Compose 中可以让这种数据绑定变得更简单,只需要分三步,按下面的代码:
/** * 定义了一个 Body 组件 * 组件本身可以传递参数,函数参数即为组件的参数 * 参数也可以是一个 Composable 组件,因此可以向组件传递另一个组件 * 从而达到类似 Flutter / React children 的效果 */ @Composable fun Body(title: String, content: @Composable () -> Unit) { // 1. 创建一个状态 var stateTitle by remember { mutableStateOf("default state") } Column { // 2. 将状态值做处理,赋值给 Text.text 属性 Text(text = "${title}_${stateTitle}") Button(onClick = { // 3. 改变状态 stateTitle += "?" }) { } content() } }
remember 和 mutableStateOf 都是 Compose 提供的函数,用于在组件中创建一个状态,mutableStateOf 创建了一个 WrapperDelegate,有 setValue 和 getValue 方法,而 remember 则将 MutableState 作为状态存储,当改变此状态值时,Compose 会进行 Recomposition 操作,重新执行 Body 函数,也就是刷新组件。
这里还用到了 Kotlin 的 by 关键字 Delegate 特性,可以去官网查阅这部分内容(不得不说 Kotlin 语法🍬真多)。
▐ 预览
Android XML 写完是可以预览的,而且速度非常快,还有可视化编辑,但 Compose 又怎么样呢?由于 Compose 实际上依赖 Kotlin 代码的编译,在预览这一块确实是不如 XML 了。在 Compose 中你得给需要预览的组件写上 @Preview 注解。
/** * 定义了一个 HomePage 组件 * 该组件使用了一个官方的 Box 容器,以及自定义的 Body 组件 */ @Composable @Preview(showBackground = true) fun HomePage() { Box { Body(title = "HomePage2333") { Text(text = "content") } } }
Compose Preview 的缺点就是慢,代码改变之后需要 rebuild 才能生效,但它具备 XML 更完善的功能,例如可以选择 interactive mode,这样可以在 Preview 下响应代码逻辑,也可以针对单个组件直接运行 App,不需要整个运行,这在调试单个组件时很有用。(当然不管哪个都还是不如 Flutter 的调试体验好就是了)
一个简单的计算器
仿 小米 12 pro 上的计算器!参加 Compose 学习挑战赛时候写着玩的。要求支持基本的加减乘除,如果能适配横屏加分。这里附上 GitHub 仓库链接,有兴趣的同学可以查看完整源码,这里只简单介绍动画和横屏适配的部分。计算器多少有很多 BUG,核心的表达式解析部分也是两年前学编译原理时候写的脚本解析器,全部都是 BUG。
Compose 计算器:https://github.com/yumeTsukiiii/ComposeCalculator
▐ 动画
视频中主要涉及到按键以及文字的缩放动画,动画其实也是通过状态驱动的,本质上,Compose 的动画 API,就是类似创建了一个 Interval 去改变状态的值,不断触发重绘。下面是按键缩放的动画逻辑:
// 按键 Scale 动画状态 @Composable fun MutableInteractionSource.animationScale(): Float { // 1. 通过 MutableInteractionSource.collectIsPressedAsState 获取 isPress 手势状态 val isPressed by collectIsPressedAsState() // 2. animateFloatAsState 创建 Float 类型的状态值 // 当 isPressed 变化时,它将从旧的值逐渐变化到新的值 val animationScale by animateFloatAsState(targetValue = if (isPressed) 0.7f else 1.0f) return animationScale } @Composable fun ScaleAnimationButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.elevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { // 3. 通过 Modifier.scale 设置 scale 的值 // 当 animationScale 返回值发生变化时,这个组件将重绘。 Button(onClick, modifier.scale(interactionSource.animationScale()), enabled, interactionSource, elevation, shape, border, colors, contentPadding, content) }
▐ 横屏适配
之前看 Google I/O 2022 时,里面有介绍 Compose 如何做大屏适配,说实话这个方法感觉也不是很好用,需要通过 if 判断去实现不同的布局,并且在不用全局状态管理的情况下还需要将 windowSize 往下传递,代码量会比较多。
class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeCalculatorTheme { // 1. 获取 windowSizeClass CalculatorApp(windowSizeClass = calculateWindowSizeClass(activity = this)) } } } } @Composable fun CalculatorApp( windowSizeClass: WindowSizeClass ) { // 2. 判断当前是否为大屏(宽大于高) if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) { // 横屏布局 } else { // 竖屏布局 } }
总结
本文简单介绍了下如何使用 Compose 开发一个简单的页面,这里面包含了一些基本的要素:定义组件、更新状态、预览页面。真实场景下使用 Compose 会非常复杂,比如页面级以及全局状态管理、导航、数据存储等,这些都需要结合 Jetpack 架构组件来使用。(虽然这和我目前工作所用到的技术和这些完全不搭边)
有关计算器的实现没有写太多,这个里面并没有用到很多 Compose 其他的知识,由于时间限制也只是用 MutableState + Component 写了很简单的 UI,没有去设计和整理。
后续可能会更新一些文章介绍下 Compose 中如何像自定义 View 那样,自己控制绘制和布局,以及它是如何渲染的等等。
最后放一个参加 GDG Compose 初级挑战赛活动获得的奖品,写计算器进阶挑战赛奖品估摸着是拿不到,毕竟大佬云集,咱只是赶工写着玩玩。



