Compose 实战练习

Compose 实战练习

这份文档包含了递进式的练习题,从简单到复杂。


📌 练习说明

  • 难度标记: ⭐ = 简单,⭐⭐ = 中等,⭐⭐⭐ = 难
  • 预计时间: 以小时计
  • 先决条件: 完成前面的学习文档

第1级:基础理解

练习 1.1:添加日志打印(⭐ 30分钟)

目标: 理解数据流的完整过程

任务:

ContactsViewModel 中的 setState()handleIntent() 中添加日志,打印:

  1. 收到哪个 Intent
  2. 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.ktInitialHeader() 函数

原代码:

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 的顶部添加一个搜索框,显示 "搜索联系人"

需要:

  1. ContactsScreen 中添加一个 TextField 组件
  2. 把搜索框放在列表上方

代码结构:

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.ktContactList() 函数

修改建议:

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 流程实践

任务:

添加一个新的功能:用户可以删除一个联系人

需要做的:

  1. ContactsIntent 中添加新的 Intent
kotlin 复制代码
sealed class ContactsIntent : Intent {
    object FetchContacts : ContactsIntent()
    data class SetContactDetail(val contact: ICallLogContact?) : ContactsIntent()
    data class DeleteContact(val contactId: String) : ContactsIntent()  // 新增
}
  1. 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()
}
  1. 在 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

任务:

添加一个"选中的联系人数量"字段,显示在页面顶部

需要做的:

  1. 扩展 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>>
  1. ContactsViewModel 中更新
kotlin 复制代码
private fun setContactDetail(contact: ICallLogContact?) {
    // ...
    // 更新选中数量
    setState { 
        copy(
            // ...,
            selectedCount = if (contact != null) 1 else 0
        )
    }
}
  1. 在 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

任务:

实现联系人搜索:用户输入文字时,列表自动过滤

需要做的:

  1. 扩展 Intent
kotlin 复制代码
sealed class ContactsIntent : Intent {
    // ...
    data class Search(val keyword: String) : ContactsIntent()
}
  1. 扩展 State
kotlin 复制代码
data class ContactsUiState(
    // ...
    val searchKeyword: String = "",  // 搜索关键词
    val filteredData: List<CallLogContactGroup>? = null  // 过滤后的数据
) : SimpleUiState<List<CallLogContactGroup>>
  1. 处理搜索 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) }
    }
}
  1. 在 UI 中使用
kotlin 复制代码
TextField(
    value = searchKeyword,
    onValueChange = { keyword ->
        contactsVM.sendIntent(ContactsIntent.Search(keyword))
    },
    label = { Text("搜索") }
)

// 显示过滤后的数据
ContactList(
    contactGroups = uiState.filteredData ?: uiState.data
)

学到的内容: 📚

  • 完整的搜索实现流程
  • 数据过滤和转换
  • 状态管理的复杂场景

练习 4.2:添加滑动删除功能(⭐⭐⭐ 2小时)

目标: 学会处理复杂的用户交互

任务:

在列表项上向左滑动时,显示删除按钮

需要做的:

这个涉及到自定义 Gesture 处理,比较复杂。

建议:

  1. 先完成前面的练习
  2. 学习 swipeToDismiss Modifier(Compose 的库函数)
  3. 逐步实现

学到的内容: 📚

  • 手势识别(Gesture)
  • 动画效果
  • 复杂的 UI 交互

第5级:性能优化

练习 5.1:分析列表性能(⭐⭐ 1小时)

目标: 学会性能调试

任务:

使用 Android Studio 的 Profiler 分析 ContactsScreen 的性能

步骤:

  1. 打开 Android Studio → View → Tool Windows → Profiler
  2. 运行应用
  3. 打开联系人页面
  4. 观察:
    • CPU 使用率
    • 内存使用
    • Frame Rate(帧率)
  5. 快速滚动列表,看是否有掉帧

优化建议:

  • 如果掉帧,考虑用 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 性能优化

💡 练习技巧

卡住了?这样做:

  1. 查看相似代码

    • 搜索项目中的类似实现
    • 比如 MainViewModel 是如何处理 Intent 的
  2. 查看文档

    • 回到 Compose学习指南.md
    • 查看相关的代码例子
  3. 使用日志调试

    kotlin 复制代码
    BaseLog.i(TAG, "调试信息: $value")
  4. Google 搜索

    • "Compose TextField 例子"
    • "Compose 搜索过滤"
  5. 提问

    • 如果真的卡住,不要死磕,改做其他练习

✅ 完成检查清单

  • 练习 1.1:日志打印能看到
  • 练习 1.2:Loading 文本已修改
  • 练习 1.3:首字母圆圈颜色已改变
  • 练习 2.1:搜索框能输入
  • 练习 2.2:列表项间距已调整
  • 练习 3.1:删除 Intent 已实现
  • 练习 3.2:选中计数已显示
  • 练习 4.1:搜索过滤工作正常
  • 练习 5.1:运行过 Profiler

每完成一个练习,✅ 打勾。


🎉 最后

完成这些练习后,你就能:

  • ✅ 理解 MVI 架构
  • ✅ 使用 Compose 构建 UI
  • ✅ 实现复杂的用户交互
  • ✅ 调试性能问题
  • ✅ 快速上手新的功能开发
相关推荐
MengFly_11 小时前
Compose案例 — Android 调用系统相机拍照
android·kotlin·compose
氦客21 小时前
Android Compose : 传统View在Compose组件中的等价物
android·compose·jetpack·对比·传统view·等价物·compose组件
氦客1 天前
UI编程的发展史 : 结合命令式UI和声明式UI
android·compose·声明式ui·ui编程·命令式ui·ui编程发展史·标记语言
stevenzqzq3 天前
Compose Navigation 时序图
compose
stevenzqzq3 天前
Compose 状态 / 协程 总图
compose
儿歌八万首6 天前
Jetpack Compose 动画实战:让你的 UI 动起来
android·kotlin·动画·compose
儿歌八万首8 天前
Jetpack Compose 自定义布局解析
kotlin·compose·自定义布局
zFox9 天前
二、Kotlin高级特性以及Compose状态驱动UI
ui·kotlin·compose
儿歌八万首10 天前
Jetpack Compose :封装 MVVM 框架
android·kotlin·compose
stevenzqzq13 天前
compose中 remember retain rememberSaveable、rememberSerializable的区别
compose