本课定位: 这是整个系列的地基。第 1 课不追求覆盖所有 API,而是建立正确的 Compose 心智模型。后面所有课程——布局、状态、导航、架构、性能——都建立在本课的概念之上。如果本课的概念没吃透,后面很容易写成"用 Compose 写 View"。
一、本课目标
学完本课,你应该能够:
二、命令式 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"
}
这段代码有两个核心问题:
命令式 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 一张对比表
| 界面来源 | 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 读取了这个状态,然后重新执行它们。这个过程叫重组。
关键点:
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 分支中,位置可能变化,状态可能被重置。这是后面学习状态管理时非常重要的细节。
四、环境准备
- Name:ComposeFirst
- Package:com.example.composefirst
- Language:Kotlin
- Minimum SDK:API 24 或更高
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 来控制尺寸、间距、点击等,提高组件复用性。
6.5 Counter:状态、事件与重组
var count by remember { mutableStateOf(0) }
这是第 1 课的核心:
- mutableStateOf(0):创建一个可观察状态,初始值为 0。
- remember:在重组之间保存这个状态。
- by:Kotlin 委托,让 count 看起来像普通变量。
当 count++ 执行时:
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 中不要写网络请求、数据库等复杂逻辑。
七、随堂练
八、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(提交):运行截图
题目要求:
- 提交运行截图。
参考答案:
建议至少提交以下截图:
解读:
- 截图不是形式,而是证明功能真实可运行。
- 截图最好包含模拟器或真机状态栏,避免只截局部导致无法判断是否运行成功。
- 如果做选做题,建议把在线和离线两种状态都截出来,体现状态驱动 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 核心概念
11.2 API 与写法
11.3 常见坑
十二、下节预告
第 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、数据流、网络、分页、测试、性能优化 | 复刻并扩展一个经典示例 |
网硕互联帮助中心




评论前必须登录!
注册