上一篇文章末尾,我说 Compose 是"真正的范式转移"。这句话听起来像吹牛,今天我要把它拆开给你看。
三年前,我也持怀疑态度。XML 不是好好的吗?findViewById 的"啰嗦"我们都已经习惯了,DataBinding 不是也能省掉 findViewById 吗?Compose 这套东西,到底新在哪?
直到我把一个真实的中等规模页面(含 3 个 RecyclerView、2 个 Tab 联动、1 个视频预览)从 View 体系重写到 Compose 之后,代码量从 1270 行 XML + Java 缩减到了 340 行 Kotlin,零 NPE、零 findViewById 报错,我才真正服了。
今天这篇文章,我们就抛开所有花哨的"对比演示",聊三个最核心的问题:
Compose 到底新在哪?
什么时候不要用 Compose?
真实迁移时该怎么做?
一、Compose 的根本创新:UI 是"状态的函数"
传统 Android(XML + View)的工作模式是:
数据 → setText() / setVisibility() → View 状态变化
每改一个 UI 字段,你都得手动调用 setXxx。漏一个就出 bug。
Compose 是反过来的:
@Composable
fun UserCard(user: User) {
Column {
Text(text = user.name, style = MaterialTheme.typography.headlineMedium)
Text(text = user.email)
}
}
UserCard 是一个函数。当 user 这个参数变化时,Compose 会自动重绘用到 user 字段的那些部分。开发者只描述"在某个状态下 UI 长什么样",剩下的交给框架。
这就是核心:UI = f(state),也叫声明式 UI。
对比一下:SwiftUI、Flutter、React 都走过这条路。Compose 是 Google 给 Android 出的这个时代的 UI 范式。
二、用一段真实代码,看清楚"省了多少"
假设有这样一个需求:根据 isLogin 状态显示头像或"请登录"按钮。
View 体系(XML + Kotlin):
<!-- activity_profile.xml -->
<FrameLayout
android:id="@+id/flAvatar"
android:layout_width="80dp"
android:layout_height="80dp">
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="请登录"
android:visibility="gone" />
</FrameLayout>
class ProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityProfileBinding
private lateinit var ivAvatar: ImageView
private lateinit var btnLogin: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
ivAvatar = binding.ivAvatar
btnLogin = binding.btnLogin
viewModel.user.observe(this) { user ->
if (user != null) {
ivAvatar.visibility = View.VISIBLE
btnLogin.visibility = View.GONE
Glide.with(this).load(user.avatarUrl).into(ivAvatar)
} else {
ivAvatar.visibility = View.GONE
btnLogin.visibility = View.VISIBLE
}
}
}
}
Compose 体系:
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
val user by viewModel.user.collectAsState()
if (user != null) {
AsyncImage(
model = user!!.avatarUrl,
contentDescription = "头像"
)
} else {
Button(onClick = { /* 跳转登录 */ }) {
Text("请登录")
}
}
}
不仅代码更少,更重要的是:
没有手动同步 UI 状态——Composable 函数读到什么 state,就画什么。
没有 findViewById——和 NPE 永久告别。
没有 XML——预览就在
@Preview注解下即时显示。
三、Compose 的三大核心概念
如果你刚接触,这三个概念必须先吃透。
1. 重组(Recomposition)
当 state 变化时,Composable 函数会重新执行——但只重新执行"用到这个 state 的部分"。
2. 状态提升(State Hoisting)
State 应该被提到最近共同祖先管理,子组件只接收 + 回调。
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
3. Side Effect(副作用)
当 Composable 进入/离开屏幕时执行的逻辑,用 LaunchedEffect、DisposableEffect 等处理。
@Composable
fun TimerScreen() {
LaunchedEffect(Unit) {
while (true) {
delay(1000)
// 更新 tick
}
}
}
记住这三点,Compose 就入门了。
四、什么场景下不要用 Compose
虽然我大力推荐 Compose,但工程永远要讲场景:
我自己的经验法则:新页面用 Compose,老页面不动。等一年,回头把老页面慢慢迁移。
五、真实迁移案例:一个订单详情页
原状
一个订单详情页,XML 嵌套 8 层(ConstraintLayout > LinearLayout > ...),代码 600 行。任何 UI 改动都要碰 5 个文件。
迁移后
Compose 单文件 90 行,所有 UI 改动只需要改一个文件。
迁移步骤
在 build.gradle 加 Compose 依赖
创建一个
setContent { OrderDetailScreen(...) }的 Activity把 XML 里的结构翻译成 Composable
把 ViewModel 保留,UI 层完全替换
用
ComposeView在 XML 页面里嵌入部分 Compose
关键坑(必看)
Glide 别忘了 androidx.compose 适配:用
accompanist-glide或迁移到 Coil/Glide 的 Compose 集成。老项目慎用
Modifier.clickable+ 自定义事件分发:注意 hit testing。预览函数(@Preview)一定要加:否则 Android Studio 的即时预览用不了。
从 Kotlin 1.5 之前的老项目迁过来:要确保 Kotlin 和 Compose Compiler 版本对齐。
六、Compose 的"性能心智模型"
Compose 的快不是因为它用了什么黑科技,而是因为它把"脏区域"的概念做对了:
跳过不必要的重组:用
remember、derivedStateOf延迟重组:用
derivedStateOf把频繁变化的 state 转成稳定的派生 state列表 LazyColumn 内部有优化:和 RecyclerView 类似,但 API 简单 N 倍
LazyColumn {
items(items = list, key = { it.id }) { item ->
OrderRow(item)
}
}
key 这个参数很关键——加上之后,列表滚动时 Compose 知道怎么复用,而不是销毁重建。
小结
Compose 不是又一个 UI 框架,它是 Android 第一次真正把"声明式 UI"做对的尝试。这套体系从上到下重新设计了:
渲染层用
DrawScope替代 Canvas API布局层用
Modifier链替代 XML 属性状态层用
MutableState替代 setter/getter
如果你还没开始学,今天回去先用 Compose 写一个简单页面,会立刻感觉到差异。
下篇预告
下一篇是《第 3 篇:Kotlin 协程——从 Thread 切到挂起函数的正确姿势》。 Compose 里大量用到协程,但很多工程师其实还停留在"协程 = 轻量 Thread"的理解上。我们下一篇会真正讲清楚协程的"挂起"到底在挂什么、为什么取消比 Thread 优雅、Dispatchers 该怎么选。
参考阅读
Jetpack Compose 官方教程
"Compose 性能:从 60FPS 到 120FPS 的路径"
《Jetpack Compose 编程实践》
上一篇文章:第 1 篇《Activity 生命周期:90% 的人都踩过这 5 个坑》