Compose 实战练习
这份文档包含了递进式的练习题,从简单到复杂。
📌 练习说明
- 难度标记: ⭐ = 简单,⭐⭐ = 中等,⭐⭐⭐ = 难
- 预计时间: 以小时计
- 先决条件: 完成前面的学习文档
第1级:基础理解
练习 1.1:添加日志打印(⭐ 30分钟)
目标: 理解数据流的完整过程
任务:
在 ContactsViewModel 中的 setState() 和 handleIntent() 中添加日志,打印:
- 收到哪个 Intent
- State 如何变化
代码位置: app/src/main/java/com/volvocars/telephony/china/ui/contacts/ContactsViewModel.kt
修改建议:
kotlin
override suspend fun handleIntent(intent: Intent) {
when (intent) {
is ContactsIntent.FetchContacts -> {
BaseLog.i(TAG, "收到 FetchContacts Intent") // 添加这行
fetchContacts()
}
is ContactsIntent.SetContactDetail -> {
BaseLog.i(TAG, "收到 SetContactDetail Intent: ${intent.contact}") // 添加这行
setContactDetail(intent.contact)
}
}
}
private suspend fun fetchContacts() {
BaseLog.i(TAG, "fetchContacts: 设置 Loading 状态")
setState { copy(loadState = LoadState.Loading) }
// ...
}
验证方法:
- 打开应用
- 打开联系人页面
- 在 Android Studio 的 Logcat 中查看日志输出
- 看日志是否按照预期流程输出
学到的内容: 📚
- 如何调试 MVI 数据流
- Intent 的完整处理过程
练习 1.2:修改 Loading 文本(⭐ 15分钟)
目标: 学会修改 UI 显示的文本
任务:
把 ContactsScreen 中的 Loading 显示文本从 "同步数据" 改成 "正在加载联系人..."
代码位置: app/src/main/java/com/volvocars/telephony/china/ui/contacts/ContactsScreen.kt
修改步骤:
kotlin
is LoadState.Loading -> {
Loading(
modifier = Modifier.fillMaxSize(),
text = "正在加载联系人..." // 修改这里
)
}
验证方法:
- 重新构建并运行应用
- 清除联系人缓存(或强制刷新)
- 看到新的加载文本
学到的内容: 📚
- 如何修改 UI 文本
when语句的实际使用
练习 1.3:改变首字母圆圈的颜色(⭐ 20分钟)
目标: 学会使用主题颜色修改 UI
任务:
把 InitialHeader 中的首字母圆圈颜色改成蓝色
代码位置: app/src/main/java/com/volvocars/telephony/china/ui/contacts/ContactsScreen.kt 的 InitialHeader() 函数
原代码:
kotlin
Box(
modifier = modifier
.padding(start = appDimens.spacing.spacing3xLarge)
.size(appDimens.heightAndWidth.size3xSmall)
.background(
color = SemanticColorKeyTokens.ForegroundL4.value, // 原颜色
shape = CircleShape
),
修改建议:
kotlin
.background(
color = SemanticColorKeyTokens.BrandBlueL1.value, // 改成蓝色
shape = CircleShape
),
验证方法:
- 重新构建并运行应用
- 打开联系人页面
- 看到首字母圆圈变成蓝色
学到的内容: 📚
- 如何使用设计令牌修改颜色
- Modifier 的
background()参数
第2级:组件理解
练习 2.1:添加搜索框(⭐⭐ 1小时)
目标: 学会添加新的 UI 组件
任务:
在 ContactsScreen 的顶部添加一个搜索框,显示 "搜索联系人"
需要:
- 在
ContactsScreen中添加一个TextField组件 - 把搜索框放在列表上方
代码结构:
kotlin
@Composable
fun ContactsScreen(...) {
val uiState by contactsVM.uiState.collectAsStateWithLifecycle()
var searchText by rememberSaveable { mutableStateOf("") } // 搜索文本状态
Column(modifier = Modifier.fillMaxSize()) {
// 搜索框
TextField(
value = searchText,
onValueChange = { searchText = it },
label = { Text("搜索联系人") },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
// 下面是原来的 Box 和列表
Box(modifier = Modifier.weight(1f)) {
when (uiState.loadState) {
// ... 原来的代码
}
}
}
}
学到的内容: 📚
- 使用
TextField组件 Column的嵌套使用Modifier.weight()分配空间
练习 2.2:改变列表的每一项高度(⭐⭐ 20分钟)
目标: 学会调整组件的大小
任务:
增加 CallContactListItem 的高度,让列表项更宽敞(比如增加 padding)
代码位置: app/src/main/java/com/volvocars/telephony/china/ui/contacts/ContactsScreen.kt 的 ContactList() 函数
修改建议:
kotlin
itemsIndexed(contactsInGroup) { index, data ->
CallContactListItem(
modifier = Modifier
.width(itemWidth)
.padding(vertical = 8.dp), // 添加竖向 padding
callContactEntry = data,
onClick = { onItemClick(data) }
)
// ...
}
验证方法:
- 重新构建并运行
- 看列表项之间的间距是否增加
学到的内容: 📚
Modifier.padding()的使用- 如何微调 UI 布局
第3级:状态管理
练习 3.1:添加新的 Intent(⭐⭐ 45分钟)
目标: 完整的 MVI 流程实践
任务:
添加一个新的功能:用户可以删除一个联系人
需要做的:
- 在
ContactsIntent中添加新的 Intent
kotlin
sealed class ContactsIntent : Intent {
object FetchContacts : ContactsIntent()
data class SetContactDetail(val contact: ICallLogContact?) : ContactsIntent()
data class DeleteContact(val contactId: String) : ContactsIntent() // 新增
}
- 在
ContactsViewModel中处理新 Intent
kotlin
override suspend fun handleIntent(intent: Intent) {
when (intent) {
is ContactsIntent.FetchContacts -> fetchContacts()
is ContactsIntent.SetContactDetail -> setContactDetail(intent.contact)
is ContactsIntent.DeleteContact -> deleteContact(intent.contactId) // 新增
}
}
private suspend fun deleteContact(contactId: String) {
BaseLog.i(TAG, "删除联系人: $contactId")
// 这里你可以调用 UseCase 删除数据
// 然后重新加载列表
fetchContacts()
}
- 在 UI 中添加删除按钮
kotlin
// 在 CallContactListItem 中添加长按事件
CallContactListItem(
modifier = Modifier.width(itemWidth),
callContactEntry = data,
onClick = { onItemClick(data) },
onLongClick = { // 新增
contactsVM.sendIntent(ContactsIntent.DeleteContact(data.id))
}
)
验证方法:
- 运行应用
- 长按某个联系人
- 看日志是否打印删除事件
学到的内容: 📚
- 完整的 Intent 定义流程
- 如何扩展现有的 MVI 系统
- 如何在 UI 中添加新的交互
练习 3.2:添加新的 State 字段(⭐⭐ 1小时)
目标: 学会扩展 State
任务:
添加一个"选中的联系人数量"字段,显示在页面顶部
需要做的:
- 扩展
ContactsUiState
kotlin
data class ContactsUiState(
override val loadState: LoadState,
override val data: List<CallLogContactGroup>?,
val dialogState: ContactDialog = ContactDialog.None,
val selectedCount: Int = 0 // 新增:选中的联系人数
) : SimpleUiState<List<CallLogContactGroup>>
- 在
ContactsViewModel中更新
kotlin
private fun setContactDetail(contact: ICallLogContact?) {
// ...
// 更新选中数量
setState {
copy(
// ...,
selectedCount = if (contact != null) 1 else 0
)
}
}
- 在 UI 中显示
kotlin
@Composable
fun ContactsScreen(...) {
Column(modifier = Modifier.fillMaxSize()) {
// 显示选中数量
if (uiState.selectedCount > 0) {
Text(
text = "已选中 ${uiState.selectedCount} 个联系人",
modifier = Modifier.padding(16.dp)
)
}
// 下面是原来的代码
Box(modifier = Modifier.weight(1f)) {
// ...
}
}
}
验证方法:
- 运行应用
- 点击某个联系人
- 页面顶部显示 "已选中 1 个联系人"
学到的内容: 📚
- 如何扩展 State
- State 和 UI 的同步
- 条件性显示 UI
第4级:高级应用
练习 4.1:实现搜索过滤(⭐⭐⭐ 2小时)
目标: 综合运用 State、Intent、Filter
任务:
实现联系人搜索:用户输入文字时,列表自动过滤
需要做的:
- 扩展 Intent
kotlin
sealed class ContactsIntent : Intent {
// ...
data class Search(val keyword: String) : ContactsIntent()
}
- 扩展 State
kotlin
data class ContactsUiState(
// ...
val searchKeyword: String = "", // 搜索关键词
val filteredData: List<CallLogContactGroup>? = null // 过滤后的数据
) : SimpleUiState<List<CallLogContactGroup>>
- 处理搜索 Intent
kotlin
override suspend fun handleIntent(intent: Intent) {
when (intent) {
// ...
is ContactsIntent.Search -> search(intent.keyword)
}
}
private suspend fun search(keyword: String) {
setState { copy(searchKeyword = keyword) }
if (keyword.isEmpty()) {
// 清空搜索,显示所有
setState { copy(filteredData = data) }
} else {
// 过滤数据
val filtered = data?.filter { group ->
group.second.any { contact ->
contact.name.contains(keyword, ignoreCase = true)
}
}
setState { copy(filteredData = filtered) }
}
}
- 在 UI 中使用
kotlin
TextField(
value = searchKeyword,
onValueChange = { keyword ->
contactsVM.sendIntent(ContactsIntent.Search(keyword))
},
label = { Text("搜索") }
)
// 显示过滤后的数据
ContactList(
contactGroups = uiState.filteredData ?: uiState.data
)
学到的内容: 📚
- 完整的搜索实现流程
- 数据过滤和转换
- 状态管理的复杂场景
练习 4.2:添加滑动删除功能(⭐⭐⭐ 2小时)
目标: 学会处理复杂的用户交互
任务:
在列表项上向左滑动时,显示删除按钮
需要做的:
这个涉及到自定义 Gesture 处理,比较复杂。
建议:
- 先完成前面的练习
- 学习
swipeToDismissModifier(Compose 的库函数) - 逐步实现
学到的内容: 📚
- 手势识别(Gesture)
- 动画效果
- 复杂的 UI 交互
第5级:性能优化
练习 5.1:分析列表性能(⭐⭐ 1小时)
目标: 学会性能调试
任务:
使用 Android Studio 的 Profiler 分析 ContactsScreen 的性能
步骤:
- 打开 Android Studio → View → Tool Windows → Profiler
- 运行应用
- 打开联系人页面
- 观察:
- CPU 使用率
- 内存使用
- Frame Rate(帧率)
- 快速滚动列表,看是否有掉帧
优化建议:
- 如果掉帧,考虑用
LazyColumn替代Column(你的代码已经用了) - 检查是否有不必要的 recomposition
学到的内容: 📚
- 性能监控工具的使用
- Recomposition 的概念
🎯 推荐的学习顺序
第1级 (今天完成)
└─ 1.1 日志打印 → 1.2 修改文本 → 1.3 改颜色
第2级 (明天完成)
└─ 2.1 搜索框 → 2.2 调整高度
第3级 (后天完成)
└─ 3.1 删除功能 → 3.2 选中数量
第4级 (一周后)
└─ 4.1 搜索过滤
第5级 (两周后)
└─ 5.1 性能优化
💡 练习技巧
卡住了?这样做:
-
查看相似代码
- 搜索项目中的类似实现
- 比如 MainViewModel 是如何处理 Intent 的
-
查看文档
- 回到
Compose学习指南.md - 查看相关的代码例子
- 回到
-
使用日志调试
kotlinBaseLog.i(TAG, "调试信息: $value") -
Google 搜索
- "Compose TextField 例子"
- "Compose 搜索过滤"
-
提问
- 如果真的卡住,不要死磕,改做其他练习
✅ 完成检查清单
- 练习 1.1:日志打印能看到
- 练习 1.2:Loading 文本已修改
- 练习 1.3:首字母圆圈颜色已改变
- 练习 2.1:搜索框能输入
- 练习 2.2:列表项间距已调整
- 练习 3.1:删除 Intent 已实现
- 练习 3.2:选中计数已显示
- 练习 4.1:搜索过滤工作正常
- 练习 5.1:运行过 Profiler
每完成一个练习,✅ 打勾。
🎉 最后
完成这些练习后,你就能:
- ✅ 理解 MVI 架构
- ✅ 使用 Compose 构建 UI
- ✅ 实现复杂的用户交互
- ✅ 调试性能问题
- ✅ 快速上手新的功能开发