技能驱动的 AI 协作开发实录:从零打造「放大镜」Android 与 iOS 应用
前言
几个月前,我想给家里有老花眼的长辈做一个打开就能用的放大镜 App:进入即放大、一键开灯、一个滑块调倍率,装到 Android 手机上。后来这个需求一路演变成了一个完整的故事——Android 版上架自托管、中英双语国际化、再复刻一个 iOS 版。
整个开发过程几乎全部由 AI 协作完成,而且不是对话式打补丁,而是一套技能(Skill)驱动的工程化流程:需求被反复拷问、词汇表与架构决策被实时记录、规范变成工单、实现严格 TDD、最后还有双轴代码评审。
这篇文章重点不是App 怎么写的,而是这套流程怎么跑起来的,以及过程中那些真正让人头疼的核心问题是怎么解决的。
需求与设计:先烤再动手(grill-with-docs)
开工前,我调用了 grill-with-docs 技能。它的设计很有意思:先别急着写代码,把设计树上的每个决策都问一遍。
整个提问分了两轮:
- 第一轮(地基):技术栈(Kotlin + Compose)、相机方案(CameraX)、默认放大倍数(2.0x)、放大范围(跟随设备最大倍率)、手势(滑块+捏合)、前后摄(仅后置)、闪光灯行为、是否要定格、横竖屏、倍率记忆、分发方式、应用名……每个问题都附带推荐答案。
- 第二轮(收尾):最低系统版本(minSdk 26)、应用图标、夜间亮度增强、是否加网格线。
这套每问必带推荐答案的节奏很关键——被提问者不需要从零思考,只需要确认或否决,决策成本极低。
同时,domain-modeling 技能要求边聊边写文档:
- 领域词汇表
CONTEXT.md:放大倍数(Zoom Ratio)、设备最大倍率、闪光灯(Torch)、定格画面(Freeze Frame)、老花眼用户(Presbyopia User)、亮度增强……每个术语都明确是什么、避免用什么词。比如刻意区分老花眼用户和老人——老花不等于年纪大。 - 架构决策 ADR:为什么选 CameraX 而不是 Camera2、为什么 minSdk 26 而不是 31。这些都是后人看了会疑惑的决定,记录下来防止未来被好心修正。
从对话到规范与工单(to-spec / to-tickets)
设计确定后,to-spec 技能把对话直接合成为一份规范,发布成 GitHub issue,并打上 ready-for-agent 标签。规范里包含 Problem Statement、Solution、15+ 条用户故事、实现决策、测试决策。
规范里有一件很重要的事:确认测试缝(seam)。整个 App 的核心逻辑被收敛到一个放大镜会话(MagnifierSession),它通过窄接口 CameraGateway 访问相机——UI 是薄壳,可测的只有会话层。测试缝只有一个,这是刻意为之:能测试的边界越少,测试越值钱。
接着 to-tickets 把规范拆成 8 张垂直切片工单:
- 每张票都是一个窄但完整的路径:骨架+默认放大 → 倍率调节 → 倍率记忆 → 闪光灯 → 定格 → 屏幕行为 → 权限 → 构建验收。
- 每张票标注阻塞边(Blocked by):01 是唯一入口,02–07 可并行,08 收尾。
- 每张票都带验收标准,打上
ready-for-agent。
这保证了任何一个 Agent 拿到任何一张票,都能在单个上下文窗口内独立完成并验证。
TDD 实现(implement + tdd)
实现阶段用的是 implement + tdd 技能。流程被严格约束:
- 先写失败测试(红灯);
- 只写让测试通过的最少代码(绿灯);
- 重构留到评审阶段,不在红绿循环里混。
举个例子,放大倍数记忆这张票跑了两个红绿循环:
- 循环一:先写上次倍率在启动时恢复恢复值超出设备范围时收敛无保存值时用默认值三个失败测试 → 引入
ZoomStore持久化接口 + 会话恢复逻辑 → 变绿。 - 循环二:再写调节后保存捏合采纳后保存范围重收敛后保存三个失败测试 → 在会话的三条应用路径上补
save→ 变绿。
tdd 技能还强调了几条反模式,实践中很有用:
- 实现耦合的测试:断言内部调用细节,重构就碎。
- 同义反复的测试:期望值用和实现相同的方式算出来,永远通过。
- 水平切片:一次性写完全部测试再实现——测试的是想象出来的行为。
测试框架踩坑:backgroundScope 不执行
写会话测试时遇到一个诡异问题:测试里用 backgroundScope.launch { flow.collect {...} } 收集相机倍率范围,但协程从头到尾没执行。我写了个最小探针测试确认这不是我代码的问题——连 backgroundScope.launch { flag = true } 都不跑。
最后把会话改为显式的 start()/stop() 生命周期(由 ViewModel 或测试持有者调用),测试用测试作用域 + 显式取消,问题消失。这是 TDD 本身带来的回报:测试逼你暴露真实的生命周期设计。
核心问题解决实录
这是这篇文章最想分享的部分。每个问题都是真实踩坑 + 排查过程。
CameraX 版本:1.6.1 看着最新,其实装不上
选 CameraX 版本时发现最新稳定版 1.6.1 的 AAR 元数据要求 minCompileSdk=36、AGP 8.9.1,而本机只有 compileSdk 35 + AGP 8.7.3。直接下载 AAR 解析元数据,确认后回退到 1.5.3(compileSdk 35、AGP 8.6+ 即可)。教训:最新稳定版不等于当前工具链能用,版本选择要用元数据验证而不是看发布时间。
默认 2.0x 倍率被占位范围吞掉
第一个真 bug:默认 2.0x 没生效,日志显示实际应用的是 1.0x。原因是我在网关里用 ZoomRange(1, 1) 作为未绑定时的占位值,会话收到占位范围后把默认 2.0x 收敛成了 1.0,等真实范围到来时已经晚了。
修复:网关改用 SharedFlow,只在绑定成功后发射真实范围——不存在虚假占位值,会话就不会提前收敛。
setZoomRatio 反馈循环把主线程压垮
模拟器上应用被系统强杀,日志里全是一条条重复的 apply zoom: 2.0。原因:CameraX 的 zoomState 持续发射,我的会话对每一次发射都重复调用 setZoomRatio,相机又因此再次发射……形成反馈循环,主线程被 GC 压垮。
修复:会话增加 appliedZoom 去重——只在值实际变化时才下发相机。修复后日志从几百条变成一条。
定格画面方向:EXIF 根本不可靠
真机反馈:竖屏时定格照片是横屏,横屏时是竖屏。根因是 ImageCapture 拿到的是传感器原始帧(横屏),方向信息靠 EXIF,而部分设备/场景下 EXIF 不可靠。
我最初方案是设置 targetRotation + 按 EXIF 旋转解码,但模拟器验证时发现 JPEG 里根本没有 EXIF 方向标签。最终方案更彻底:直接截取 PreviewView 当前显示的画面(getBitmap()),捕捉到的就是预览所见,方向、裁剪天然一致,任何手机朝向都不会错。这就是所见即所得比事后矫正更可靠。
从设置页授权返回后权限不刷新
权限被拒 → 去设置页授权 → 返回 App,界面仍显示需要权限。原因是权限状态只在请求回调里更新,从设置返回不会触发回调。
修复:在生命周期 ON_RESUME 重新检查权限,授权回来后自动绑定相机。这种跨页面返回的状态同步,生命周期监听比回调可靠。
iOS 模拟器没有相机:控件被合理地禁用了
iOS 版在模拟器上出现两个问题:没有闪光灯按钮、滑块拖不动。排查后确认根因是模拟器没有摄像头:绑定失败 → torchAvailable=false(按钮隐藏)、maxZoomRatio=0(滑块禁用)。
修复策略:
- 滑块在相机范围未知时用兜底范围 1–8x,保证可交互;真机绑定后自动切换真实范围。
- 闪光灯按钮在相机不可用(如模拟器)时也显示但置灰;真机有闪光灯时可用;仅真机有相机但无闪光灯时隐藏。
同时修复了会话的一个隐患:相机范围未知时设置倍率只更新期望值、不下发相机,避免状态与实际不同步。
双轴代码评审(code-review)
implement 流程的最后一步是 code-review:从 Standards(代码规范/坏味道) 和 Spec(是否实现规范要求) 两个独立维度并行评审,最后分开报告,不做跨维度综合排名。
Standards 轴带有一套固定的坏味道基线(Fowler《重构》第 3 章):神秘命名、重复代码、特性依恋、投机泛化、中间人……评审时逐条对照。
Spec 轴则逐条对照验收标准,报告三类问题:缺失/部分实现、越界行为、实现可疑点。曾发现会话 clamp 不一致(默认值用 (min, max) 收敛、用户输入用 (1, max) 收敛)这样的隐藏问题,当场修复并补测试。
评审子代理跑偏事件
code-review 技能的设计是两个并行子代理分别评审两轴。实践中有两次子代理严重跑偏:
- 一个子代理没收到任务内容,回复请把任务发给我;
- 更严重的一次,子代理直接在工作区里实现了另一张票的功能(把定格画面的代码写进了工作区),还声称已提交推送。
处理方式:立即中断、用 git 核对提交内容与我的实现是否一致(确认无夹带)、还原被污染的文件、之后评审改由主代理亲自执行。教训:给子代理的任务要显式声明只读,禁止改任何文件,并在子代理结束后核对工作区是否被改动。
成果盘点
| 维度 | 数据 |
|---|---|
| GitHub issue | 1 份 spec + 8 张实现工单,全部关闭 |
| Android 单元测试 | 26 个,覆盖倍率/记忆/闪光灯/定格/错误状态 |
| iOS 单元测试 | 18 个(XCTest) |
| 文档 | CONTEXT.md 词汇表、6 份 ADR、2 份 spec、真机验收清单 |
经验总结
- 决策前置比实现重要。grill-with-docs 把十几个决策在写代码前全部钉死,实现阶段几乎不需要回头改需求。
- 文档跟着决策走。词汇表、ADR、spec 都在决策发生的当下落盘,而不是事后补——事后补的文档基本都是错的。
- 测试缝要少而准。一个会话缝 + 假网关,26 个测试撑起整个业务逻辑;UI 保持薄壳。
- TDD 的回报是设计。backgroundScope 踩坑逼出了显式生命周期;测试先行的代码往往比先实现再补测试更干净。
- 用元数据验证版本兼容,用最小复现验证框架行为,用日志验证真实链路——猜不如查。
- AI 协作要设护栏:子代理任务里显式声明只读、完成后核对工作区、关键操作(提交/推送)由主代理统一执行。
- 所见即所得优先于事后矫正:定格截取预览帧,比任何 EXIF 旋转逻辑都可靠。
如果这篇文章能让你对技能驱动的 AI 协作开发有一个具体印象——从需求拷问到发布归档的完整闭环——那它就算达到目的了。
项目与发布
- 项目仓库
- zoomin(Android,public):源码与 README 已公开,家人可直接下载安装。
zoomin-ios(iOS,private):SwiftUI + AVFoundation 实现,复用同一套会话 + 网关协议架构。
- 正式发布(GitHub Releases)
- Android v1.0.0 / v1.1.0:Releases 提供 APK 下载,附 SHA-256 校验。
- 正式签名包:独立密钥库(RSA 2048、10000 天)签名,release 构建开启 R8 混淆(APK 从 12MB 缩到 2.3MB),
apksigner校验通过。
- v1.1 国际化:新增
values-en英文资源,界面跟随系统语言,非中英语言回退中文,英文应用名 ZoomIn。用 Lint 的 MissingTranslation 检查中英一一对应。 - iOS 版:本机没有模拟器运行时,下载了 8.5GB 的 iOS 26.5 运行时;最终 18 个 XCTest 全部通过,模拟器实机验证了权限流程、错误提示、中英双语切换。


