首页 / Android 入门教程 / Compose 编程思想

Android 入门教程

Compose 编程思想

本教程共 100 篇 · 第 42 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程Jetpack Compose声明式UI可组合函数重组

42. Compose 编程思想

本节目标:理解声明式 UI 和命令式 UI 的本质区别,搞懂可组合函数的特性、重组是怎么回事,为后续 Compose 学习打下思维基础。

从命令式到声明式

先打个比方。命令式 UI 像做饭时一步步下指令:先切菜、再起锅、放油、倒菜、翻炒、加盐。你精确控制每一步,锅里的状态被你反复修改。

声明式 UI 像点外卖:你只要告诉系统「我要一份番茄炒蛋」,至于怎么做、怎么翻炒,系统自己搞定。你描述的是「最终要什么样」,不是「怎么一步步做到」。

传统 Android View 体系是命令式的。你用 XML 写布局,代码里用 findViewById 拿到控件,然后 setText()setVisibility() 一个个改。数据一变,你得记得手动更新所有相关控件,漏了一个界面就不一致。

Jetpack Compose 是声明式的。你用 Kotlin 函数描述界面长什么样,数据变了,框架自动重新计算并更新界面。你不用手动操控控件,只需描述「这个数据对应这个界面」。

可组合函数

Compose 的核心是可组合函数(Composable Function)。用 @Composable 注解标记一个普通 Kotlin 函数,它就能描述界面:

@Composable
fun Greeting(name: String) {
    Text("你好,$name!")
}

这个函数接受数据(name),发出界面元素(Text)。它有几个关键特性:

  • @Composable 注解:告诉 Compose 编译器这是界面函数。
  • 接受数据作为参数:数据描述了界面应该长什么样。
  • 调用其他可组合函数:通过组合简单组件构建复杂界面。
  • 没有返回值:函数描述界面状态,不返回控件对象。
  • 快速、幂等、无副作用:同样的输入永远得到同样的结果。

声明式的工作流

在命令式世界里,你改数据后要手动更新视图。在声明式世界里,流程是这样的:

  1. 组合(Composition):Compose 首次执行你的可组合函数,生成界面描述。
  2. 状态变化:用户点击或数据更新,触发状态改变。
  3. 重组(Recomposition):Compose 用新数据重新执行可组合函数,更新界面。
数据变化 -> 重组 -> 界面更新

关键是第三步:你不需要手动告诉界面「这里改了,那里改了」。 只要数据变了,Compose 自动找到受影响的部分重新执行。

重组(Recomposition)

重组是 Compose 的核心机制,也是最容易踩坑的地方。

打个比方:重组像考试改卷。老师(Compose)不需要把整份卷子重新批改一遍,只改你涂改过的那道题。Compose 也会智能跳过参数没变的函数,只重新执行参数有变化的部分。

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("点了 $clicks 次")
    }
}

每次点击,clicks 变了,Text 会被重组。但如果旁边有个显示其他数据的 Text,它不会被重新执行—因为它的参数没变。

Warning

重组有几个重要特性,写代码时必须记住:

  1. 重组会跳过尽可能多的内容:只有参数变化的函数才重组。
  2. 重组是乐观的:Compose 认为参数会变完就停,如果参数又变了,可能取消当前重组重新开始。
  3. 可组合函数可能非常频繁地执行:动画时可能每帧都执行。
  4. 可组合函数可以并行执行:Compose 可能在多线程上同时执行不同的可组合函数。
  5. 可组合函数可以按任意顺序执行:不能假设先写的先执行。

无副作用原则

因为重组的这些特性,可组合函数必须没有副作用。副作用是指对函数外部世界产生的影响,比如:

  • 修改全局变量
  • 写文件
  • 更新 ViewModel 里的数据
  • 调用 Random()

为什么?因为 Compose 可能跳过、取消、重试、并行执行你的函数。如果函数有副作用,执行次数和时机都不可预测,界面就会出 bug。

// 错误:在可组合函数里修改局部变量计数
@Composable
fun BadExample(items: List<String>) {
    var count = 0  // 别这么写
    Column {
        for (item in items) {
            Text(item)
            count++  // 危险!重组时 count 会乱掉
        }
        Text("共 $count 项")
    }
}

// 正确:直接用数据计算
@Composable
fun GoodExample(items: List<String>) {
    Column {
        for (item in items) {
            Text(item)
        }
        Text("共 ${items.size} 项")
    }
}
Tip
  • 如果确实需要执行副作用(比如网络请求、文件读写),应该通过回调触发(如 onClick),或使用专门的副作用 API(LaunchedEffect 等,第 54 章会讲),而不是直接在函数体里写。

动态内容

可组合函数是纯 Kotlin 代码,所以你拥有语言的全部能力。可以用 if 控制显示,用 for 循环生成列表,调用辅助函数:

@Composable
fun GreetingList(names: List<String>) {
    Column {
        if (names.isEmpty()) {
            Text("暂无数据")
        } else {
            for (name in names) {
                Text("你好,$name!")
            }
        }
    }
}

这是 Compose 相比 XML 的巨大优势:界面逻辑直接用 Kotlin 写,不用数据绑定、不用模板语言,所见即所得。

Compose 的优势

总结一下 Compose 为什么好:

  • 代码简洁:一个函数描述一个组件,比 XML + Java/Kotlin 两套文件清晰。
  • 类型安全:编译期就能发现错误,不像 XML 的字符串引用运行时才报。
  • 没有手动更新:数据变了界面自动更新,不用记得调 setText
  • 可复用:函数组合天然支持复用,比自定义 View 简单太多。
  • Kotlin 原生:if/for/when 直接用,界面逻辑和业务逻辑同一种语言。
Note
  • Compose 不是要消灭 View 体系。现有项目可以逐步迁移,新旧可以共存(第 55 章会讲互操作)。但新项目推荐直接用 Compose。

小结

思维转变是学 Compose 的第一步。记住:你不再「操控控件」,而是「描述界面和数据的关系」。可组合函数要快、幂等、无副作用,把副作用交给回调和专门的 API 处理。理解了重组机制,后面写代码就不容易踩坑了。