每日单词 App 迭代四:20 词总览模式,两场 AI 对话从计划到落地
「每日单词」是我用 Kotlin + Jetpack Compose + Room + 系统 TTS 做的 Android 单词学习 App:内置 500 个基础词,每天固定学 20 个新词,配合间隔复习和连续天数打卡。9 月 1 日到 9 月 6 日,我通过和 AI 编码助手的两场对话完成了第四个迭代——20 词总览学习模式。这篇记录两场对话的分工、关键设计取舍和踩过的坑。
为什么要有总览模式
原来的学习流程是逐词卡片:一次只看一个词,点「认识 / 不认识」翻下一个。用了一段时间发现一个问题:有时候我想先扫一眼今天 20 个词的整体难度,再决定从哪里开始认真看;逐词模式强迫我按顺序一个个翻,没有「总览」的视角。
于是我写了一份开发计划文档,描述一个「20 词同屏」的总览模式:整组词列表展示、行内揭示释义、作答进度与逐词模式完全打通。
两场对话的分工:一场主建,一场主审
这次迭代最值得记录的不是功能本身,而是协作方式。我开了两场独立的 AI 对话,交错进行:

图 1:对话一主建、对话二主审,产物交错流动
| 对话 | 时间 | 职责 |
|---|---|---|
| 对话一:评审学习概览开发计划 | 9/1 晚 – 9/2 晚 | 按开发计划实现总览模式,随后按我的反馈重做交互 |
| 对话二:审查 20 词总览学习模式代码 | 9/1 晚 – 9/6 晚 | 代码审查 → 修复打磨 → 提交合并 → 新增自动朗读 |
流程是这样的:对话一把功能做出来后,我把它的交付总结直接粘给对话二,要求从性能、代码简洁度、影响面、正确性四个维度审查。对话二发现的缺陷修完、提交 08a2ff2 之后,又回头审查了对话一后半段的交互重做,确认无问题后提交 f83340d 并 fast-forward 合并到 main。一场产出、一场把关,两边都不既当运动员又当裁判。
对话一:先采访,再动手
我把开发计划丢给 AI 时明确允许它提意见、改计划,还可以反过来采访我。它通读代码后确认计划整体健全(批次推导、只读查看口径都能覆盖),然后采访了 3 个计划里没定死的决策点:
- 发音键的形态 → 我选「揭示释义后才出现小发音键」;
- 作答按钮的位置 → 行内展开;
- 要不要记住上次的学习模式 → 本次不做,观察实际使用后再说。
这个「采访」环节很值:三个点都是计划文档里模糊的交互细节,与其让它猜,不如 30 秒问清楚。
实现层面的核心决策是不存第二份进度:新增的 LearnOverviewViewModel 批次完全由现有的 queue + idx 派生,作答直接复用 answerToday(),数据库和 Repository 零改动。好处是完成页统计、连续天数结算全部免费复用,和逐词模式天然同口径。UI 上新增 LearnOverviewScreen(整组 20 词同屏、左侧序号色带、行内揭示、自动滚动定位当前行),导航接入 learn-overview 路由,首页和完成页各加入口。
功能做完后在 meiri_api34 模拟器上用驱动脚本跑了 14 个关键状态逐屏验证,包括杀进程重启后进度完整恢复(重启后截屏与中断前逐字节一致)、20/20 完成态与逐词模式口径一致、追加批次等。
交互重做:揭示与作答解耦
第一版做完我发现一个不顺手的地方:为什么只有第一行能点「点击显示」?AI 解释了来龙去脉——这是计划里刻意定下的「顺序不变量」取舍(进度模型只持久化连续答到第几个),不是 bug。但它给出了两全的改法:
- 揭示与作答解耦:20 行全部可以自由预览/收起释义,纯 UI 预览不写学习进度;「认识 / 不认识」按钮仍然只出现在当前待答行。作答保持从上到下的顺序,进度数据、复习排期、连续天数完全不受影响。想自测就别点、想先看就随便看。
- 卡片样式重做:每行右侧固定浅灰圆角「点击显示」占位块,点击后同一位置原地变成中文释义;已答行常显「释义 + 已认识/不认识」标签,淡绿/淡红底色区分。AI 调了视觉子代理分析我发的参考截图,把布局落到代码里。
单测从 12 例扩到 16 例,新增的两条「非当前行可预览且可收起」「预览行作答无效」把这个语义钉死。
对话二:一个真正值钱的缺陷
四维度审查的结论整体不错:主题零偏离、20 行规模下性能无负担(data class 结构相等 + items(key=word.id),实际只有作答行重绘)。但发现了一个真正值钱的缺陷:
[!warning] 跨天缺陷(P2)
总览 ViewModel 只在 init 时读一次仓库状态,不订阅。如果页面在前台跨天(onResume → refresh()会按新的一天重建队列),界面会停在昨天的批次上,所有点击静默失效。更要命的是老的逐词LearnViewModel有同款问题且更危险——它的answer(known)没有当前词 id 守卫,跨天后点「认识」会把答案写到新一天第一个词上,属于真实的数据污染路径。
这就是「另一场对话审查」的价值:写代码的那场对话对自己的设计有路径依赖,换个视角才能看到「页面在前台跨过零点」这种边界场景。
修复按根因做、不只修总览页:两个 ViewModel 都改成 repo.state.collect { rebuild() } 订阅仓库状态;逐词模式额外给 answer() 加当前词 id 守卫,仓库已变更而界面尚未重建的窗口期内拒绝写入。配套新增 5 个测试把跨天重建、清空→EMPTY、复习翻状态、过期作答被拒全部钉住,全套单测 89 例全绿。
9 月 6 日追加:点击自动朗读
我提了个体验需求:点「点击显示」展开释义的同时自动播放读音,不想再手动点喇叭。实现很克制——只改 LearnOverviewScreen 一个文件:把发音播放状态从喇叭键内部提升到行级,占位块点击时 onReveal() + playWord() 一起调,喇叭键保留作手动重听,ViewModel 和数据层零改动。
验证做了三级证据:89 例单测全绿;全流程回归零失败;新增驱动脚本从 logcat 抓到点击时刻的 GoogleTTSServiceImpl 合成请求,配合播放中/播完两张截图。这部分改动目前还未提交,留待审阅。
踩坑记录
- uiautomator 冷启动超时:模拟器刚开机 + 刚装包 +
pm clear后,Compose 首帧能超过脚本「3 秒等待 + 6 秒重试」的窗口,第一个点击就丢、后续全链失败。解法是脚本开头主动轮询欢迎页出现再动手。 - PowerShell 中文定位失效:命令行内联传中文给 XPath 定位时疑似被 GBK 转码导致 0 匹配,改用
-File方式执行 UTF-8 脚本文件后一切正常。 - 截图分析工具的 CDN 旧缓存:同名重传的截图可能命中上一轮的缓存,把完成态误读成旧中断态。解法是改名重传 + 局部裁剪复核。
- 「脚本跑完 = 没问题」不可靠:好几个问题(作答按钮被右侧释义区挤成竖排三行、流程中途回到欢迎页)都是靠逐张回看截图才发现的。工具输出与截图要相互印证。
数字盘点
2 个提交(08a2ff2 26 个文件 +1155/-25、f83340d),JVM 单测 84 → 89 例全绿,模拟器 14 个关键状态逐屏验证,12+ 张证据截图,3 个可重放的驱动脚本。
待办还有几项:自动朗读改动待审阅合入;「记住上次学习模式」等总览模式实际用几次后再决定;SerifLg 令牌语义上是「弹层标题」,现在借作行内释义,视觉没问题但语义是借用——这些小债先记着,不急着还。