云计算百科
云计算领域专业知识百科平台

【Jetpack Compose声明式UI界面开发实用技术学与练】第 1 课:从命令式到声明式——环境搭建与第一个 Compose 界面

本课定位: 这是整个系列的地基。第 1 课不追求覆盖所有 API,而是建立正确的 Compose 心智模型。后面所有课程——布局、状态、导航、架构、性能——都建立在本课的概念之上。如果本课的概念没吃透,后面很容易写成"用 Compose 写 View"。


一、本课目标

学完本课,你应该能够:

  • 理解命令式 UI 与声明式 UI 的本质区别。
  • 理解 Compose 的核心公式:UI = f(state)。
  • 使用 Android Studio 创建 Jetpack Compose 项目。
  • 理解 @Composable、setContent、@Preview 的作用。
  • 使用 Text、Button、Column、Row、Modifier 编写简单界面。
  • 使用 remember 和 mutableStateOf 实现计数器。
  • 初步理解组合、重组、状态读取追踪、位置记忆。
  • 初步掌握状态提升与单向数据流。

  • 二、命令式 UI 与声明式 UI 的本质区别

    2.1 命令式 UI:手动找控件,手动改控件

    传统 Android View 写法:

    val textView = findViewById<TextView>(R.id.textView)
    val button = findViewById<Button>(R.id.button)

    var count = 0

    button.setOnClickListener {
    count++
    textView.text = "点击次数:$count"
    }

    这段代码有两个核心问题:

  • 状态和视图是分离的。 count 是状态,TextView 是视图,你需要手动保持它们一致。
  • 每次状态变化,都要手动更新对应视图。 如果界面有 10 个地方依赖 count,你就要写 10 次更新逻辑。漏掉一个,界面就错了。
  • 命令式 UI 的本质:

    我告诉系统:先创建这个控件,再找到它,再修改它。

    2.2 声明式 UI:描述界面,框架负责更新

    Compose 的核心思想:

    UI = f(state)

    意思是:界面是状态的函数。

    你只描述:

    • 当状态是 A 时,界面应该长什么样。
    • 当状态变成 B 时,界面应该长什么样。

    你不需要手动找控件,也不需要手动改控件。状态变化后,Compose 会重新执行相关 @Composable 函数,自动更新界面。

    @Composable
    fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
    Text("点击次数:$count")
    Button(onClick = { count++ }) {
    Text("+1")
    }
    }
    }

    这里你只写了"界面如何根据 count 显示"。当 count 变化时,Text 自动刷新。

    2.3 声明式不是没有命令,而是命令被框架接管

    Compose 底层仍然要创建节点、测量、布局、绘制。但这些命令不再由你手写,而是由 Compose 运行时根据你的声明自动完成。

    更准确的理解:

    你负责声明 UI 与状态的关系;
    Compose 负责在状态变化时,以最小代价更新 UI。

    2.4 一张对比表

    维度命令式 View声明式 Compose
    界面来源 XML + 手动 findViewById @Composable 函数
    状态与视图 分离,需手动同步 状态驱动,自动同步
    更新方式 手动调用 setText 等 状态变化触发重组
    复用单位 自定义 View / 布局 Composable 函数
    组合方式 继承、组合 函数调用、组合
    预览 XML Preview @Preview

    2.5 思维方式的转变

    从命令式转向声明式,最大的挑战不是 API,而是思维习惯:

    命令式思维声明式思维
    我要把文字改成"5" 当 count 是 5 时,文字显示"5"
    我要把按钮隐藏 当条件不满足时,按钮不存在
    我要把颜色改成红色 当状态是警告时,颜色是红色
    先做什么,再做什么 状态是什么,界面就是什么

    这个转变贯穿整个系列。每次写 Compose,都要问自己:我在描述状态,还是在操作控件?


    三、Compose 运行模型:组合、重组、状态追踪

    这一节是第 1 课最值得深入理解的部分。API 可以查文档,但运行模型决定了你能不能写出正确的 Compose 代码。

    3.1 @Composable 不是普通函数

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

    @Composable 函数经过 Compose 编译器插件处理后,会带有额外的运行时信息。它不能被普通函数随意调用,通常只能在另一个 @Composable 函数中调用。

    可以这样理解:

    普通函数:执行逻辑,返回结果。
    Composable:描述 UI,参与组合。

    3.2 组合:Composable 执行后形成的 UI 描述树

    当 Compose 第一次执行 @Composable 函数时,会生成一棵"组合树",也叫 Composition。

    Column {
    Text("标题")
    Button(onClick = { }) {
    Text("按钮")
    }
    }

    粗略理解为:

    Column
    ├── Text("标题")
    └── Button
    └── Text("按钮")

    这棵树就是 Compose 管理界面的依据。

    3.3 重组:状态变化后,重新执行相关 Composable

    当某个状态发生变化,Compose 会找出哪些 @Composable 读取了这个状态,然后重新执行它们。这个过程叫重组。

    关键点:

  • 重组不是重建整个 Activity。
  • 重组不是重新执行所有 Composable。
  • Compose 会尽量跳过没有变化的节点。
  • 重组是增量、智能、可跳过的。
  • 3.4 状态读取追踪

    Compose 之所以知道"谁该重组",是因为它会在 Composable 执行期间记录:

    谁读取了哪个 State。

    当 State 变化时,Compose 通知读取过它的 Composable 重新执行。

    这就是为什么普通变量不行:

    var count = 0
    Text("$count")

    普通变量不是 State,Compose 无法追踪读取,也无法在变化时触发重组。

    3.5 幂等性:Composable 应该可以安全地重复执行

    Composable 可能因为状态变化而多次执行。因此它应该满足:

  • 多次执行结果一致,只要输入和状态相同。
  • 不产生不可控副作用。
  • 不依赖执行顺序。
  • 不直接修改外部变量。
  • 错误示例:

    @Composable
    fun Bad() {
    loadDataFromNetwork() // 每次重组都会执行,可能重复请求
    Text("内容")
    }

    正确做法是把副作用放到 LaunchedEffect、DisposableEffect 等 API 中,第 10 课会深入。

    3.6 位置记忆:remember 为什么能保存状态?

    remember 是按"调用位置"来记忆的。Compose 在组合树中为每个 remember 记录一个位置。只要这个位置在重组中保持稳定,remember 保存的值就会保留。

    var count by remember { mutableStateOf(0) }

    重组时,Compose 发现还是同一个位置,于是取出之前保存的 MutableState。

    但如果 remember 出现在 if 分支中,位置可能变化,状态可能被重置。这是后面学习状态管理时非常重要的细节。


    四、环境准备

  • 安装 Android Studio 稳定版。
  • 新建项目:New Project > Empty Activity。
  • 配置:
    • Name:ComposeFirst
    • Package:com.example.composefirst
    • Language:Kotlin
    • Minimum SDK:API 24 或更高
  • 等待 Gradle Sync 完成。
  • 启动模拟器或连接真机,点击 Run。
  • Empty Activity 模板通常会自动加入 Compose 依赖。关键依赖:

    implementation(platform("androidx.compose:compose-bom:…"))
    implementation("androidx.activity:activity-compose:…")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    版本以 Android Studio 模板生成或官方文档为准。


    五、第一个完整 Compose 程序

    打开 MainActivity.kt,写入以下完整示例:

    package com.example.composefirst

    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.layout.*
    import androidx.compose.material3.*
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.compose.ui.unit.dp

    class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
    MyApp()
    }
    }
    }

    @Composable
    fun MyApp() {
    MaterialTheme {
    Surface(modifier = Modifier.fillMaxSize()) {
    Column(
    modifier = Modifier.padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(24.dp)
    ) {
    Greeting(name = "Compose 学习者")
    Counter()
    }
    }
    }
    }

    @Composable
    fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column(
    modifier = modifier,
    verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    Text(
    text = "你好,$name!",
    style = MaterialTheme.typography.headlineSmall
    )
    Text(
    text = "欢迎进入 Jetpack Compose 声明式 UI 世界",
    style = MaterialTheme.typography.bodyMedium
    )
    }
    }

    @Composable
    fun Counter(modifier: Modifier = Modifier) {
    var count by remember { mutableStateOf(0) }

    Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Text(
    text = "点击次数:$count",
    style = MaterialTheme.typography.titleLarge
    )

    Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
    Button(
    onClick = { count— },
    enabled = count > 0
    ) {
    Text("-1")
    }

    Button(onClick = { count++ }) {
    Text("+1")
    }

    OutlinedButton(onClick = { count = 0 }) {
    Text("重置")
    }
    }

    if (count >= 5) {
    Text(
    text = "不错,已经点击 $count 次!",
    color = MaterialTheme.colorScheme.primary
    )
    }
    }
    }

    @Preview(showBackground = true, name = "首页预览")
    @Composable
    fun MyAppPreview() {
    MyApp()
    }

    注意:by remember 需要导入 getValue 和 setValue,使用 import androidx.compose.runtime.* 即可。


    六、代码逐层深入拆解

    6.1 setContent:进入 Compose 世界的入口

    setContent {
    MyApp()
    }

    setContent 是 Activity 中设置 Compose 界面的入口。它内部会创建 Compose 的运行环境,把 MyApp() 放入组合树中。

    类比传统 View:

    setContentView(R.layout.activity_main)

    区别:setContent 接收的是一段声明式 UI,而不是一个 XML 布局。

    6.2 MaterialTheme 与 Surface

    MaterialTheme {
    Surface(modifier = Modifier.fillMaxSize()) {
    // 内容
    }
    }

    • MaterialTheme:提供 Material 3 的颜色、字体、形状等设计系统。
    • Surface:提供一个有背景、可承载内容的表面,通常用于页面根容器。

    6.3 Column:垂直布局

    Column(
    modifier = Modifier.padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(24.dp)
    ) {
    Greeting(...)
    Counter()
    }

    • Column 把子元素从上到下排列。
    • verticalArrangement 控制主轴,也就是垂直方向上的排列。
    • Arrangement.spacedBy(24.dp) 表示子元素之间间隔 24dp。
    • modifier = Modifier.padding(16.dp) 给整个 Column 增加内边距。

    6.4 Greeting:Composable 参数与 Modifier 约定

    Compose 组件常见的参数约定:

  • 必填数据参数放前面。
  • modifier: Modifier = Modifier 放后面。
  • 可选参数再往后。
  • 这样做的好处:调用方可以统一传入 Modifier 来控制尺寸、间距、点击等,提高组件复用性。

    6.5 Counter:状态、事件与重组

    var count by remember { mutableStateOf(0) }

    这是第 1 课的核心:

    • mutableStateOf(0):创建一个可观察状态,初始值为 0。
    • remember:在重组之间保存这个状态。
    • by:Kotlin 委托,让 count 看起来像普通变量。

    当 count++ 执行时:

  • MutableState 的值发生变化。
  • Compose 发现 Text("点击次数:$count") 读取了 count。
  • 相关 Composable 被重新执行。
  • Text 显示新的次数。
  • 6.6 Row 与按钮

    • Row 把子元素从左到右排列。
    • horizontalArrangement 控制水平方向排列。
    • enabled = count > 0 让 count == 0 时"-1"按钮禁用。
    • Button、OutlinedButton 是 Material 3 提供的按钮组件。

    6.7 条件渲染

    if (count >= 5) {
    Text("不错,已经点击 $count 次!")
    }

    Compose 中可以直接用 if 控制 UI 是否出现。它和传统 View 的 visibility 不同:

    • if 为 false 时,这段 UI 不进入组合。
    • if 为 true 时,这段 UI 进入组合。
    • 不是隐藏,而是"存在或不存在"。

    6.8 @Preview

    @Preview 让 Android Studio 直接预览 UI,不必运行模拟器。常用参数:

    • showBackground = true:显示背景。
    • name = "首页预览":预览名称。
    • widthDp、heightDp:指定预览尺寸。
    • group:预览分组。

    Preview 中不要写网络请求、数据库等复杂逻辑。


    七、随堂练

  • 把问候语改成"你好,XXX!我是 Compose 新手"。
  • 给计数器增加"-1"按钮,且 count == 0 时禁用。
  • 增加"重置"按钮,把次数归零。
  • 当 count >= 5 时,显示一句鼓励文字。
  • 新建一个 @Preview,单独预览 Counter()。

  • 八、15 道习题(含参考答案与解读)

    习题 1:Compose 声明式 UI 的核心公式是什么?

    参考答案: UI = f(state)

    解读: 界面是状态的函数。状态是唯一数据源,UI 是状态在当前时刻的投影。状态变化后,Compose 重新执行读取了该状态的 Composable,从而更新界面。声明式 UI 的重点不是"怎么改控件",而是"状态是什么,界面就应该是什么"。这个公式看似简单,但它是理解后面所有 Compose 行为的总纲。


    习题 2:在 Compose 中,描述 UI 的函数必须加什么注解?函数命名有什么建议?

    参考答案: 必须加 @Composable;函数名建议首字母大写。

    解读: @Composable 不是普通注解,它会让 Compose 编译器插件参与处理,使函数能参与组合、重组和状态追踪。命名首字母大写是为了和普通 Kotlin 函数区分,例如 Greeting()、Counter()、LikeButton()。这也是 Compose 官方代码规范。如果写成小写,虽然能编译,但可读性会下降,团队协作中容易混淆。


    习题 3:Activity 中进入 Compose 的入口函数是什么?写出最小调用形式。

    参考答案:

    setContent {
    MyApp()
    }

    解读: setContent 是 Activity 进入 Compose 世界的入口。它替代了传统 View 体系中的 setContentView。内部会创建 Compose 运行环境,把你的 Composable 放入组合树中。没有 setContent,Activity 就不会显示 Compose 界面。它本质上是 ComponentActivity 的扩展函数,所以必须继承 ComponentActivity 或其子类。


    习题 4:@Preview 的作用是什么?为什么预览函数里不建议写复杂业务逻辑?

    参考答案: @Preview 用于在 Android Studio 中直接预览 Composable,不必运行 App。预览函数应只负责展示 UI,不写复杂业务逻辑。

    解读: Preview 是开发期工具,不是真实运行环境。它适合快速查看布局、主题、间距、文字效果。如果写网络请求、数据库、复杂副作用,可能导致预览失败、卡顿或结果不可控。预览函数最好保持纯粹,只传入假数据。另外,@Preview 函数本身也是 @Composable,但它不会被真实 App 调用,所以适合放模拟数据。


    习题 5:写出一行代码:声明一个初始值为 0 的可观察状态,并在重组之间保存。

    参考答案:

    var count by remember { mutableStateOf(0) }

    解读: mutableStateOf(0) 创建可观察状态;remember 让状态在重组之间不被重置;by 是 Kotlin 委托语法,简化读写。三者配合,才能让 count 在界面中既"能被观察",又"能跨重组保存"。如果只写 mutableStateOf 不写 remember,重组后状态可能丢失。如果只写 remember 不写 mutableStateOf,变化又无法被 Compose 观察。


    习题 6:为什么不能写 var count = 0,然后 Button(onClick = { count++ })?

    参考答案: 普通局部变量不是 Compose 状态,变化后不会触发重组,界面不会更新。

    解读: Compose 只观察 State 对象。mutableStateOf 返回的是 MutableState,它属于 Compose 的快照状态系统。普通变量没有订阅机制,Compose 不知道它变了,也不会重新执行 UI。所以界面看起来"点了没反应"。这也是初学者最常见的错误之一:代码没错,但界面不刷新。


    习题 7:什么是重组?

    参考答案: 当 Compose 读取的状态发生变化时,Compose 重新执行相关 @Composable 函数,刷新界面,这个过程叫重组。

    解读: 重组是 Compose 的核心机制。它不是重建整个 Activity,也不是重新执行所有 Composable。Compose 会记录哪些 Composable 读取了哪些状态,只重新执行受影响的部分,并尽量跳过没有变化的节点。因此重组通常是增量、智能、可优化的。理解这一点,后面学习性能优化时就会很自然。


    习题 8:Column 中让子元素水平居中,应设置哪个参数?

    参考答案:

    Column(
    horizontalAlignment = Alignment.CenterHorizontally
    ) {
    // 子元素
    }

    解读: Column 的主轴是垂直方向,交叉轴是水平方向。horizontalAlignment 控制子元素在水平方向的对齐方式。类似地,Row 的主轴是水平方向,交叉轴是垂直方向,所以 Row 用 verticalAlignment 控制垂直对齐。理解"主轴 / 交叉轴"是掌握 Compose 布局的关键。


    习题 9:Row 中给子元素之间统一加 12dp 间距,应怎么写?

    参考答案:

    Row(
    horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    // 子元素
    }

    解读: Arrangement 控制主轴排列方式。spacedBy(12.dp) 表示子元素之间统一间隔 12dp。它比手动插入 Spacer 更简洁,也更适合统一间距。注意:spacedBy 只影响子元素之间,不影响两端。如果需要在两端也留白,应配合 padding 使用。


    习题 10:Modifier.padding(16.dp) 的作用是什么?

    参考答案: 给当前组件增加 16dp 内边距。

    解读: Modifier 是 Compose 的链式配置工具,可设置尺寸、背景、边框、点击、内边距等。padding 影响组件内容与边界之间的距离。需要特别注意 Modifier 顺序:padding 后 background 和 background 后 padding 效果不同。顺序不同,修饰范围不同。这是第 3 课会深入的内容,但第 1 课就要有"Modifier 顺序有语义"的意识。


    习题 11:只在 count >= 5 时显示一句鼓励文字,写出代码片段。

    参考答案:

    if (count >= 5) {
    Text("不错,已经点击 $count 次!")
    }

    解读: Compose 中可以直接用 if 决定是否插入 UI。它和传统 View 的 visibility 不同:if 为 false 时,这段 UI 不进入组合;为 true 时,才进入组合。这是声明式 UI 非常自然的条件渲染方式。背后的原理是:Composable 就是函数,函数里能用 if、when、for 等 Kotlin 控制流,UI 会随控制流动态生成。


    习题 12:remember 和 mutableStateOf 各自解决什么问题?

    参考答案: mutableStateOf 创建可观察状态;remember 让状态在重组之间不被重置。

    解读: 两者职责不同,通常配合使用。mutableStateOf 解决"变化能被 Compose 观察到"的问题;remember 解决"重组后状态还在不在"的问题。只写 mutableStateOf 不写 remember,状态可能在重组后丢失;只写 remember 不写状态类型,变化又无法被 Compose 观察。若要在屏幕旋转、配置变更后仍保存,还要进一步使用 rememberSaveable。


    习题 13:按钮在 count == 0 时禁用,怎么写?

    参考答案:

    Button(
    onClick = { count— },
    enabled = count > 0
    ) {
    Text("-1")
    }

    解读: enabled = false 时按钮不可点击,Material 组件通常还会自动显示禁用样式。这体现了声明式 UI 的思想:你不去手动设置按钮颜色和点击状态,而是声明"当 count > 0 时按钮可用",框架根据状态决定外观和行为。这种"声明条件,而非操作结果"的思维方式,是 Compose 开发的核心习惯。


    习题 14:状态提升:把 Counter 内部的 count 提升到父组件,写出父与子的关键签名。

    参考答案:

    @Composable
    fun CounterHost() {
    var count by remember { mutableStateOf(0) }

    Counter(
    count = count,
    onCountChange = { count = it }
    )
    }

    @Composable
    fun Counter(
    count: Int,
    onCountChange: (Int) -> Unit
    ) {
    Button(onClick = { onCountChange(count + 1) }) {
    Text("+1:$count")
    }
    }

    解读: 状态由父组件持有,子组件只接收数据和事件回调,形成单向数据流:

    状态向下传递,事件向上回调。

    这样做的好处:Counter 变成无状态组件,更容易复用、测试和预览;父组件掌握状态,更容易协调多个子组件。状态提升是 Compose 架构中非常重要的基础模式,第 4 课和第 9 课会继续深入。


    习题 15:综合题:写一个 LikeButton,显示点赞数,点击 +1,并给出 @Preview。

    参考答案:

    @Composable
    fun LikeButton(modifier: Modifier = Modifier) {
    var likes by remember { mutableStateOf(0) }

    Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    Text(text = "点赞数:$likes")
    Button(onClick = { likes++ }) {
    Text("点赞")
    }
    }
    }

    @Preview(showBackground = true)
    @Composable
    fun LikeButtonPreview() {
    MaterialTheme {
    LikeButton()
    }
    }

    解读: 这个小题综合了状态、事件、重组、布局和预览。likes 是状态,Button 触发事件,Text 读取状态并自动刷新。MaterialTheme 包一层可以让 Preview 更接近真实主题效果。实际项目中,进一步优化可以把 likes 提升到父组件,让 LikeButton 变成无状态组件,这样它就能在不同页面复用。


    九、作业(含参考答案与解读)

    作业 1(必做):完成课堂计数器

    题目要求:

    • 支持 +1、-1、重置。
    • 次数不能小于 0。
    • 至少一个 @Preview。

    参考答案:

    @Composable
    fun Counter(modifier: Modifier = Modifier) {
    var count by remember { mutableStateOf(0) }

    Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Text(
    text = "点击次数:$count",
    style = MaterialTheme.typography.titleLarge
    )

    Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
    Button(
    onClick = {
    if (count > 0) count—
    },
    enabled = count > 0
    ) {
    Text("-1")
    }

    Button(onClick = { count++ }) {
    Text("+1")
    }

    OutlinedButton(onClick = { count = 0 }) {
    Text("重置")
    }
    }

    if (count >= 5) {
    Text(
    text = "不错,已经点击 $count 次!",
    color = MaterialTheme.colorScheme.primary
    )
    }
    }
    }

    @Preview(showBackground = true, name = "计数器预览")
    @Composable
    fun CounterPreview() {
    MaterialTheme {
    Counter()
    }
    }

    解读:

    • count 是唯一状态,界面完全由它驱动。
    • enabled = count > 0 让"-1"按钮在 0 时不可点击。
    • onClick 里再写 if (count > 0) count– 是双重保护,避免极端情况下出现负数。
    • if (count >= 5) 是 Compose 的条件渲染,不是隐藏,而是决定这段 UI 是否进入组合。
    • @Preview 可以单独预览 Counter,不需要运行整个 App。

    作业 2(选做):制作"个人名片"页面

    题目要求:

    • 显示姓名、简介、技能标签。
    • 一个按钮切换"在线/离线"状态。
    • 在线时文字为绿色,离线时为灰色。

    参考答案:

    @Composable
    fun ProfileCard(modifier: Modifier = Modifier) {
    var isOnline by remember { mutableStateOf(true) }

    val statusColor = if (isOnline) Color(0xFF2E7D32) else Color.Gray
    val statusText = if (isOnline) "在线" else "离线"

    Column(
    modifier = modifier
    .fillMaxWidth()
    .padding(24.dp),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Box(
    modifier = Modifier
    .size(80.dp)
    .clip(CircleShape)
    .background(MaterialTheme.colorScheme.primary),
    contentAlignment = Alignment.Center
    ) {
    Text(
    text = "张",
    color = Color.White,
    style = MaterialTheme.typography.headlineMedium,
    fontWeight = FontWeight.Bold
    )
    }

    Text(
    text = "张三",
    style = MaterialTheme.typography.headlineSmall
    )

    Text(
    text = "Android 开发者,热爱 Compose",
    style = MaterialTheme.typography.bodyMedium
    )

    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
    SkillTag("Kotlin")
    SkillTag("Compose")
    SkillTag("Android")
    }

    Row(
    verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    Box(
    modifier = Modifier
    .size(10.dp)
    .clip(CircleShape)
    .background(statusColor)
    )
    Text(
    text = statusText,
    color = statusColor
    )
    }

    Button(onClick = { isOnline = !isOnline }) {
    Text(if (isOnline) "切换为离线" else "切换为在线")
    }
    }
    }

    @Composable
    private fun SkillTag(text: String) {
    Surface(
    shape = MaterialTheme.shapes.small,
    color = MaterialTheme.colorScheme.secondaryContainer
    ) {
    Text(
    text = text,
    modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
    style = MaterialTheme.typography.labelMedium
    )
    }
    }

    @Preview(showBackground = true, name = "个人名片预览")
    @Composable
    fun ProfileCardPreview() {
    MaterialTheme {
    ProfileCard()
    }
    }

    解读:

    • isOnline 是状态,statusColor 和 statusText 是由状态派生出来的值。
    • 点击按钮后修改 isOnline,读取它的 Text、Box、Button 会自动重组。
    • SkillTag 被拆成独立 Composable,体现组件化思想,后续可以复用。
    • Modifier 顺序很重要:size -> clip -> background 才能得到圆形背景;如果顺序错误,背景可能不是圆形。
    • 实际项目中可以进一步做状态提升,把 isOnline 提到父组件,让 ProfileCard 变成无状态组件,更容易测试和预览。

    作业 3(提交):代码文件或 Git 仓库链接

    题目要求:

    • 提交代码文件或 Git 仓库链接。

    参考答案:

    项目结构建议:

    ComposeFirst/
    ├── app/
    │ └── src/main/java/com/example/composefirst/
    │ └── MainActivity.kt
    └── README.md

    Git 提交命令示例:

    git init
    git add .
    git commit -m "第1课作业:计数器与个人名片"
    git branch -M main
    git remote add origin <你的仓库地址>
    git push -u origin main

    README.md 模板:

    # 第 1 课作业

    ## 功能

    – 计数器:+1、-1、重置,次数不小于 0
    – 个人名片:姓名、简介、技能标签、在线/离线切换

    ## 运行环境

    – Android Studio 稳定版
    – Kotlin
    – Jetpack Compose
    – Material 3

    ## 截图

    (在此处插入运行截图)

    解读:

    • 提交代码时不要提交 build/、.gradle/ 等生成目录。
    • 推荐使用 .gitignore 忽略构建产物。
    • README.md 能让别人快速知道项目做了什么、怎么运行。
    • 如果是课堂提交,至少保证项目能编译、能运行、有 @Preview。

    作业 4(提交):运行截图

    题目要求:

    • 提交运行截图。

    参考答案:

    建议至少提交以下截图:

  • 计数器初始状态:点击次数:0
  • 点击 +1 到 5 次:显示鼓励文字
  • 点击重置:回到 点击次数:0
  • 个人名片"在线"状态
  • 个人名片"离线"状态
  • 解读:

    • 截图不是形式,而是证明功能真实可运行。
    • 截图最好包含模拟器或真机状态栏,避免只截局部导致无法判断是否运行成功。
    • 如果做选做题,建议把在线和离线两种状态都截出来,体现状态驱动 UI 的效果。

    作业 5(提交):200 字学习总结

    题目要求:

    • 写 200 字学习总结:什么是声明式 UI?什么是重组?

    参考答案:

    本课让我理解了 Compose 的声明式思想:UI = f(state)。传统 View 需要先 findViewById,再手动修改控件;而 Compose 只需要声明状态与界面的关系。计数器例子中,count 用 remember 和 mutableStateOf 保存,点击按钮改变状态后,读取 count 的 Text 会自动重组。我还学会了 Column、Row、Modifier、@Preview 的基本用法,以及用 if 做条件渲染。作业中通过"-1"按钮禁用和"重置"按钮,加深了对 enabled 和状态驱动界面的理解。重组不是重建整个 Activity,而是状态变化后重新执行相关 Composable。后续我会继续学习布局、状态提升和架构,写出更可复用的 Compose 组件。

    解读:

    • 总结中应出现关键词:声明式 UI、UI = f(state)、状态、remember、mutableStateOf、重组、Composable、状态驱动。
    • 不要只写"学会了按钮和文字",要写清楚"为什么状态变化能自动刷新界面"。
    • 如果能结合自己踩过的坑,例如"普通变量不会触发重组",总结会更有深度。

    十、常见坑与调试建议

  • setContent 找不到
    检查 activity-compose 依赖是否配置。

  • by remember 报错
    导入 androidx.compose.runtime.getValue 和 setValue,或使用 import androidx.compose.runtime.*。

  • Preview 不刷新
    执行 Build > Make Project,检查 @Preview 是否加在 Composable 上。

  • 状态不更新
    确认使用了 remember 和 mutableStateOf,而不是普通变量。

  • 在 Composable 中写副作用
    不要在 Composable 里直接发网络请求、写数据库、改外部变量。副作用应放到 LaunchedEffect、DisposableEffect 等 API 中。

  • 无限重组
    如果在 Composable 执行期间直接修改状态,可能导致无限重组。状态修改应发生在事件回调或副作用中。

  • Composable 命名不规范
    建议首字母大写,便于识别。

  • Modifier 顺序混乱
    Modifier 顺序会影响实际效果。例如 padding 和 background 的先后顺序会导致背景范围不同。

  • remember 位置变化
    remember 依赖调用位置。如果它出现在条件分支中,位置变化可能导致状态重置。

  • 需要跨配置变更保存状态
    第 1 课先用 remember。如果要屏幕旋转后仍保存,需要学习 rememberSaveable。


  • 十一、本节课知识点总结

    11.1 核心概念

  • 声明式 UI 核心思想:UI = f(state),状态驱动界面。
  • 命令式与声明式区别:命令式手动找控件、改控件;声明式描述状态与 UI 的关系,由框架更新。
  • 组合:Composable 执行后形成的 UI 描述树。
  • 重组:状态变化后,重新执行相关 Composable 的过程。
  • 状态读取追踪:Compose 记录谁读取了哪个状态,从而精准触发重组。
  • 位置记忆:remember 按调用位置保存状态。
  • 幂等性:Composable 应能安全地重复执行,不产生不可控副作用。
  • 11.2 API 与写法

  • Compose 入口:Activity 中使用 setContent { … }。
  • Composable 函数:使用 @Composable 注解,函数名建议首字母大写。
  • 预览工具:使用 @Preview 在 Android Studio 中直接查看 UI。
  • 状态管理基础:mutableStateOf 创建可观察状态,remember 跨重组保存状态。
  • 基础布局:Column 垂直排列,Row 水平排列,Box 层叠排列。
  • 对齐与间距:Arrangement 控制主轴排列,Alignment 控制交叉轴对齐。
  • Modifier:链式配置 UI,如 padding、size、background、clickable 等。
  • 条件渲染:Compose 中可直接用 if 控制某段 UI 是否显示。
  • 状态提升:状态由父组件持有,子组件接收数据和事件回调,形成单向数据流。
  • 11.3 常见坑

  • 普通变量不会触发重组。
  • 忘记导入 getValue / setValue。
  • 在 Composable 中写复杂副作用。
  • Preview 不刷新时先 Make Project。
  • Modifier 顺序错误导致效果不符。
  • remember 位置变化导致状态重置。

  • 十二、下节预告

    第 2 课:布局系统:用 Column、Row、Box 搭出个人名片。

    你将掌握:

    • Column 垂直布局
    • Row 水平布局
    • Box 层叠布局
    • Spacer 间距
    • Arrangement 与 Alignment
    • 用布局组合出真实页面结构
    • 个人名片实战:头像、姓名、简介、技能标签、社交按钮

    附:系列 20 课规划(已更新最后两课)

    课次主题核心内容实战产出
    1 开课与第一个 Compose 声明式 UI、环境搭建、@Composable、@Preview 计数器
    2 布局系统 Column、Row、Box、Spacer、Arrangement、Alignment 个人名片
    3 Modifier 与样式 padding、size、background、border、clip、clickable 卡片样式
    4 状态与重组 remember、mutableStateOf、State Hoisting 购物车数量
    5 列表与脚手架 LazyColumn、LazyRow、Scaffold、TopAppBar 待办列表
    6 输入与表单 TextField、Checkbox、Switch、RadioButton 登录表单
    7 主题与设计系统 Material 3、ColorScheme、Typography、Shape、暗黑模式 一键换肤
    8 导航 Navigation Compose、路由、参数、底部导航 三页导航
    9 架构与状态管理 ViewModel、UiState、单向数据流、Repository 架构版计数器
    10 副作用 LaunchedEffect、DisposableEffect、rememberCoroutineScope 模拟加载数据
    11 动画 animate*AsState、AnimatedVisibility、Crossfade、Transition 展开卡片
    12 手势与交互 clickable、pointerInput、draggable、swipe 滑动删除
    13 自定义布局与绘制 Layout、Canvas、DrawScope、SubcomposeLayout 环形进度条
    14 性能优化 重组范围、稳定性、derivedStateOf、key 优化长列表
    15 测试 Compose UI Test、语义、单元测试 测试计数器
    16 互操作 AndroidView、ComposeView、View/Fragment 混合 嵌入旧 View
    17 工程化 多模块、Hilt/Koin、资源、预览、调试工具 模块拆分
    18 响应式与自适应 WindowSizeClass、平板、折叠屏、多窗口 自适应列表
    19 经典示例深度解读一 选取经典 Compose 示例,深度解读布局、状态、列表、主题、动画、导航等核心实现 拆解并重写关键页面
    20 经典示例深度解读二 选取经典架构示例,深度解读 ViewModel、数据流、网络、分页、测试、性能优化 复刻并扩展一个经典示例
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Jetpack Compose声明式UI界面开发实用技术学与练】第 1 课:从命令式到声明式——环境搭建与第一个 Compose 界面
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!