本文简单介绍了下如何使用 Compose 开发一个简单的页面,这里面包含了一些基本的要素:定义组件、更新状态、预览页面。有关 Compose 的架构,以及它的渲染原理,可以期待以后的文章。
<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 里一些基本的要素,有了这些,就可以写一个简单的页面了!
与 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()
}
}
/**
* 必须继承 ComponentActivity
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 调用 setContent 函数
setContent {
HomePage()
}
}
}
到这里为止,其实就可以将 App 运行起来了。
Android dataBinding XML 和 ViewModel 示例,来自我的 GitHub 仓库中三年前的项目。
Compose 中可以让这种数据绑定变得更简单,只需要分三步,按下面的代码:
/**
* 定义了一个 Body 组件
* 组件本身可以传递参数,函数参数即为组件的参数
* 参数也可以是一个 Composable 组件,因此可以向组件传递另一个组件
* 从而达到类似 Flutter / React children 的效果
*/
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 组件
*/
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
算术脚本解析器:https://github.com/yumeTsukiiii/atri_script
视频中主要涉及到按键以及文字的缩放动画,动画其实也是通过状态驱动的,本质上,Compose 的动画 API,就是类似创建了一个 Interval 去改变状态的值,不断触发重绘。下面是按键缩放的动画逻辑:
// 按键 Scale 动画状态
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
}
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: Unit RowScope.() ->
) {
// 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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeCalculatorTheme {
// 1. 获取 windowSizeClass
CalculatorApp(windowSizeClass = calculateWindowSizeClass(activity = this))
}
}
}
}
fun CalculatorApp(
windowSizeClass: WindowSizeClass
) {
// 2. 判断当前是否为大屏(宽大于高)
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
// 横屏布局
} else {
// 竖屏布局
}
}
本文简单介绍了下如何使用 Compose 开发一个简单的页面,这里面包含了一些基本的要素:定义组件、更新状态、预览页面。真实场景下使用 Compose 会非常复杂,比如页面级以及全局状态管理、导航、数据存储等,这些都需要结合 Jetpack 架构组件来使用。(虽然这和我目前工作所用到的技术和这些完全不搭边)
有关计算器的实现没有写太多,这个里面并没有用到很多 Compose 其他的知识,由于时间限制也只是用 MutableState + Component 写了很简单的 UI,没有去设计和整理。
后续可能会更新一些文章介绍下 Compose 中如何像自定义 View 那样,自己控制绘制和布局,以及它是如何渲染的等等。
最后放一个参加 GDG Compose 初级挑战赛活动获得的奖品,写计算器进阶挑战赛奖品估摸着是拿不到,毕竟大佬云集,咱只是赶工写着玩玩。
我们是大淘宝技术原生研发模式团队,负责集团原生研发模式DX的建设与演进,除了服务淘宝核心链路(首页/详情/交易/消息/订阅/我淘等)外,集团内接入数十百App、承载数百亿日PV、服务数千开发者,在为集团各App提供更好的开发者体验和消费者体验的基础上,我们正致力于探索和建立下一代大终端研发模式。详细介绍见:https://www.yuque.com/liuyue-cjd4a/yief2r/la4gs2。大淘宝技术 2023 届校园招聘正在绝赞进行中,欢迎对客户端原生技术感兴趣的同学来和我们一起搞事情!简历可投递至 wxw266107@alibaba-inc.com(邮件和简历附件请以「岗位-姓名-内推」命名)。
作者|王小伟(柳月)
编辑|橙子君