Administrator
发布于 2026-08-25 / 26 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 2 篇:为什么 Android 工程师都在转 Compose?

上一篇文章末尾,我说 Compose 是"真正的范式转移"。这句话听起来像吹牛,今天我要把它拆开给你看。

三年前,我也持怀疑态度。XML 不是好好的吗?findViewById 的"啰嗦"我们都已经习惯了,DataBinding 不是也能省掉 findViewById 吗?Compose 这套东西,到底新在哪?

直到我把一个真实的中等规模页面(含 3 个 RecyclerView、2 个 Tab 联动、1 个视频预览)从 View 体系重写到 Compose 之后,代码量从 1270 行 XML + Java 缩减到了 340 行 Kotlin,零 NPE、零 findViewById 报错,我才真正服了。

今天这篇文章,我们就抛开所有花哨的"对比演示",聊三个最核心的问题:

  1. Compose 到底新在哪?

  2. 什么时候不要用 Compose?

  3. 真实迁移时该怎么做?


一、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 为主)

⚠️ 渐进式迁移,新页面用 Compose

老项目重构

⚠️ 看 ROI,部分页面用 Compose + View interop

Compose 还没稳定的部分(如复杂的第三方 View 嵌套)

❌ 用传统 View

团队完全没人懂 Kotlin

❌ 准备好再上

我自己的经验法则:新页面用 Compose,老页面不动。等一年,回头把老页面慢慢迁移。


五、真实迁移案例:一个订单详情页

原状

一个订单详情页,XML 嵌套 8 层(ConstraintLayout > LinearLayout > ...),代码 600 行。任何 UI 改动都要碰 5 个文件。

迁移后

Compose 单文件 90 行,所有 UI 改动只需要改一个文件。

迁移步骤

  1. 在 build.gradle 加 Compose 依赖

  2. 创建一个 setContent { OrderDetailScreen(...) } 的 Activity

  3. 把 XML 里的结构翻译成 Composable

  4. 把 ViewModel 保留,UI 层完全替换

  5. 用 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 个坑》


评论