Jetpack Compose UI组件Mock回放实战 - 第4节

Mock 数据是什么,为什么我们需要它

你要是做过 UI 开发,一定遇到过这种情况:后端接口还没写好,页面没法跑。或者接口偶尔报错,一刷新数据就变。你想想看,调试 UI 根本没法进行。

我刚开始做 Compose 项目时,也经常被这个问题卡住。后来发现,提前准备好一套 Mock 数据,问题就迎刃而解了。

说白了,Mock 数据就是「假数据」。它模仿真实数据的结构,用来在开发阶段替代真实后端响应。它可能是一个 List、一个 data class 实例,也可能是一个模拟的 API 调用。

为什么我们非用不可?我给你列几个理由:

  • 解耦前后端:前端不再依赖后端进度,可以并行开发。
  • 稳定复现:特定状态(空列表、网络超时、边界值)可以通过 Mock 数据准确复现。
  • 快速迭代:修改 Mock 数据比重启后端快得多,尤其适合 Compose 的实时预览。
  • 单元测试基础:没有 Mock 数据,你没法对 ViewModel 或 UI 组件进行自动测试。
核心观点:Mock 数据是 Compose UI 开发的“基础设施”,它不是随便写几个常量,而是一套精心设计的“数据剧本”。

Mock 数据的使用场景

我归纳了 4 个最常见的场景。嗯,每一个我都踩过坑,你留意一下。

场景一:开发阶段的 UI 预览

Compose 的 @Preview 注解配合 Mock 数据,能让你在没运行 App 时就看清 UI 效果。我个人习惯在每个 Composable 函数旁放一个 Preview 函数,传入 Mock 数据。

@Preview(showBackground = true)
@Composable
fun UserCardPreview() {
    val mockUser = User(
        id = "001",
        name = "张三",
        avatarUrl = "https://example.com/avatar.png",
        isVip = true
    )
    UserCard(user = mockUser)
}

这样,我只需改一行 Mock 数据,就能切换不同状态测试。

场景二:单元测试中的稳定输入

我不希望测试用例每次运行都依赖网络或数据库。Mock 数据可以构造各种边界情况:空列表、超长文本、空字符串、非法数值。我记得有一次线上崩溃就是因为后端返回了一个 null 字段,而我当时只测试了正常数据。后来我再写测试时,一定会在 Mock 数据里塞进 null 和空值。

场景三:ViewModel / StateFlow 的快速调试

在开发过程中,你往往需要模拟用户操作后的状态变化。比如用户点击“收藏”按钮后,页面从“未收藏”变“已收藏”。用 Mock 数据构建多个状态对象,在 ViewModel 里用 MutableStateFlow 切换,效果立竿见影。

场景四:原型演示与设计验收

在项目早期,后端接口根本没影。我会用一套 Mock 数据 + Compose 预览给设计师看效果。设计师可以当场要求修改颜色、间距,我再改一下 Mock 数据,几秒后就能看到新版本。这比用 Figma 转静态图高效多了。

使用场景 Mock 数据的作用 我的经验教训
UI 预览 快速测试布局与状态 别忘了 Mock 数据要和真实数据结构完全一致
单元测试 构造边界与异常情况 我曾经只测试 happy path,上线就被 null 击穿
ViewModel 调试 模拟状态切换 用 sealed class 管理多个 Mock 状态更方便
原型演示 展示完整交互流程 Mock 数据要包含足够多的字段,不然设计师会吐槽“太假”

如何定义高质量的 Mock 数据

方式一:硬编码 data class 实例

简单粗暴,适合单个组件预览。缺点是维护性差,改动时需要到处改。

val mockUser = User(
    id = "001",
    name = "张三",
    avatarUrl = "https://via.placeholder.com/100",
    isVip = false,
    points = 0
)

方式二:工厂函数与默认参数

我个人比较喜欢这种方式。写一个顶层函数,提供默认值,外部只覆盖需要修改的字段。这样代码量少,也容易看清差异。

fun mockUser(
    id: String = "default_id",
    name: String = "默认用户",
    avatarUrl: String = "https://default.avatar",
    isVip: Boolean = false,
    points: Int = 0
): User = User(id, name, avatarUrl, isVip, points)

// 使用时只需要指定变化值
val vipUser = mockUser(isVip = true, points = 1000)
val emptyUser = mockUser(name = "", points = -1)

方式三:使用 Mock 库(如 MockK、Mockito)

如果你在做集成测试,或者需要模拟复杂的异步行为,可以考虑 MockK。不过对于纯 UI 组件,我建议优先用硬编码或工厂函数,因为 Mock 库会引入额外依赖,而且调试时不容易直接看到数据。

小技巧:在 Compose 模块里,可以把 Mock 数据定义在单独的 mock 包下,和生产代码完全隔离。这样你发布 release 时,用资源集排除掉这个包就行。

避坑指南(我踩过的坑)

  • Mock 数据与真实数据结构不一致:我曾经让后端改了一个字段名,我忘了更新 Mock 数据,结果预览正常,运行后直接崩溃。后来我用同一份 data class 生成 Mock,彻底解决。
  • 只覆盖正常情况:我之前只测了列表有数据的情况。结果线上出现空列表时,UI 显示了一个半残的提示图。现在我的 Mock 数据集一定包含 null、空列表、超长文本、负数、特殊符号。
  • Mock 数据过于“完美”:真实数据可能包含网络延迟、加载失败、图片 404。我建议你的 Mock 数据也模拟这些状态:用一个延迟 Flow、或者直接返回一个错误对象。
重要提醒:永远不要在 Mock 数据中包含真实的用户隐私信息。即使只在开发环境使用,也要小心提交到公开仓库。我一般用伪造的姓名和占位头像。

Mock 数据的使用流程

下面这张图展示了我个人常用的 Mock 数据使用流程。从定义开始,到预览、测试、演示,最后随着后端迭代逐步替换为真实数据。

Mock 数据使用流程 定义 Mock 数据 UI 预览 单元测试 原型演示 评估是否满足需求 逐步替换为真实数据 评估条件 • UI 状态是否覆盖完整 • 边界情况是否通过测试 • 后端接口是否可用了

从图里可以看到,定义 Mock 数据后它会流向三个方向:UI 预览、单元测试、原型演示。这三个方向的输出都汇聚到“评估”节点,只有评估通过后,我们才逐步用真实数据替换 Mock 数据。

你可能注意到我特意画了一条从“评估”回到“定义 Mock 数据”的回流。没错,在迭代过程中,你很容易发现自己的 Mock 数据不够全面——比如漏了一种错误状态或者一种布局变化。这时候就需要补充 Mock 数据,然后重新测试。

我的习惯:我会在项目根目录下放一个 mock_data 文件夹,里面放各种状态的 Mock 数据文件(Kotlin 源码)。每次新增或修改字段时,顺手更新这个文件夹里的所有 Mock 数据。虽然一开始麻烦,但后期能省大量调试时间。

好了,关于 Mock 数据的概念和使用场景,我基本都讲到了。概括一下:Mock 数据不是一次性工具,而是伴随整个开发周期的“数据基础设施”。你需要不断根据新的需求补充和调整它,就像维护代码一样。