23、Jetpack Compose入门:Compose声明式UI思想、可组合函数、布局与修饰符、状态管理与重组、Compose与传统View混合开发
说实话,第一次接触Compose的时候,我心里是有点抵触的。毕竟在Android圈子里摸爬滚打了好几年,XML写布局早就成了肌肉记忆。但当我真正花了一个周末把一个小模块用Compose重写之后,嗯,真香。今天我们就来聊聊这个改变Android UI开发方式的东西。
声明式UI思想:不是“怎么画”,而是“是什么”
传统的View体系,说白了是命令式的。你要告诉系统:先创建一个TextView,再设置它的文字,然后把它放到LinearLayout里,最后设置一下边距。每一步都是指令,像在指挥一个机器人干活。
Compose不一样。它是声明式的。你只需要描述UI“应该长什么样”,剩下的交给框架。举个例子:
// 传统方式
val textView = TextView(context)
textView.text = "Hello"
textView.setTextColor(Color.RED)
layout.addView(textView)
// Compose方式
Text("Hello", color = Color.Red)
看到区别了吗?Compose里没有“创建”、“添加”这些动作。你直接说“这里有个红色的Hello文字”,它就出来了。我在项目中遇到过很多同事刚开始写Compose时,总想着去拿View的引用做操作,其实完全不需要。
核心思想:UI = f(state)。界面是状态的函数。状态变了,界面自动更新。你不用手动去findViewById,也不用操心什么时候该刷新。
可组合函数:最小的UI积木
在Compose里,一切UI都是函数。用@Composable注解标记的函数,就叫可组合函数。你可以把它理解成乐高积木,一块一块拼起来。
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
@Composable
fun MyScreen() {
Column {
Greeting("小明")
Greeting("小红")
}
}
我个人习惯把可组合函数分成三类:
- 叶子组件:只包含基础组件,比如Text、Image、Button
- 容器组件:负责布局,比如Column、Row、Box
- 业务组件:组合了数据和UI,比如用户卡片、商品列表
你想想看,这种分层方式是不是比传统View的继承体系清爽多了?没有复杂的基类,没有抽象工厂,就是函数调函数。
布局与修饰符:给UI加点“料”
Compose的布局组件不多,但够用。Column(纵向排列)、Row(横向排列)、Box(层叠排列),这三个基本能覆盖90%的场景。
真正让Compose灵活的是修饰符(Modifier)。它就像流水线,一个接一个地给组件添加属性。
@Composable
fun StyledCard() {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.clickable { /* 点击事件 */ }
.shadow(4.dp, shape = RoundedCornerShape(8.dp))
) {
Text("点我试试", modifier = Modifier.padding(8.dp))
}
}
修饰符的顺序很重要。我曾经踩过一个坑:把.clickable放在了.padding前面,结果点击区域还是整个屏幕宽度,因为padding是在点击之后才生效的。记住,修饰符是从左到右、从外到内依次应用的。
小技巧:如果你需要给多个组件加相同的修饰符,可以提取成一个变量。比如 val commonModifier = Modifier.padding(8.dp).fillMaxWidth(),然后到处复用。
状态管理与重组:Compose的“心脏”
Compose最神奇的地方在于:状态变了,UI自动更新。这背后靠的是“重组”(Recomposition)。
简单来说,当某个状态发生变化时,Compose会重新执行依赖这个状态的可组合函数,只更新变化的部分,不会全量刷新。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
这里remember的作用是让状态在重组时不被重置。mutableStateOf创建了一个可观察的状态。当count变化时,只有Text会重组,Button不会。
为什么会这样?因为Compose在编译期就分析出了哪些代码块依赖了count。这种细粒度的重组,性能比传统的notifyDataSetChanged好太多了。
注意:不要在可组合函数里做耗时操作或IO操作。重组可能会被频繁触发,导致性能问题。我见过有人直接在Composable里调网络请求,结果界面卡成PPT。
Compose与传统View混合开发:不是二选一
很多项目不可能一夜之间全换成Compose。好在Google考虑到了这一点,Compose可以和传统View共存。
在XML布局里嵌入Compose:
// 在Activity或Fragment中
setContentView(R.layout.activity_main)
// 在XML中
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// 在代码中
findViewById<ComposeView>(R.id.compose_view).setContent {
MyComposable()
}
反过来,在Compose里使用传统View:
@Composable
fun AndroidViewExample() {
AndroidView(
factory = { context ->
TextView(context).apply {
text = "我是传统TextView"
textSize = 18f
}
},
modifier = Modifier.fillMaxWidth()
)
}
我个人建议的策略是:新功能用Compose写,旧功能逐步迁移。不要一上来就重构整个项目,风险太大。我曾经在一个中型项目里尝试全量迁移,结果光是兼容旧的自定义View就花了两周。
知识体系总览
下面这张图帮你理清Compose的核心脉络:
总结一下
Compose不是要取代传统View,而是给了我们另一种选择。它的声明式思想让UI代码更直观,可组合函数让复用变得简单,修饰符链式调用比XML属性更灵活,状态管理机制则彻底解放了开发者。
如果你刚开始学,我建议从一个小模块入手,比如写一个登录页面或者用户信息卡片。先感受一下“状态变了UI自动更新”的爽快感,再慢慢深入。别急着把所有东西都塞进Compose,循序渐进才是正道。
一句话总结:Compose让Android UI开发从“怎么画”变成了“是什么”,从“手动刷新”变成了“自动重组”。这不仅仅是工具的变化,更是思维方式的升级。