1. 为什么我要给一块旧手表单独写个番茄钟
手里这块华米Amazfit GTR4大概是两年前入的,当时图的是续航和轻便,日常戴着看通知、记步数、测心率都挺顺手。但用久了就发现一个尴尬的事:我真正需要的"番茄工作法"和"待办清单"这两个功能,它自带的系统里要么没有,要么做得极其简陋——自带的番茄钟只能计时,不能跟任务绑定;待办事项又藏在很深的菜单里,抬手看一眼要划好几下。市面上主流的番茄类App基本都盯着手机端,手表端要么是阉割版,要么干脆没有。
我试过几个方案:一是用手机上的番茄App,但每次开始专注都得掏手机,反而增加了分心的机会;二是用系统自带的倒计时,但它跟任务系统是割裂的,做完一个番茄不知道对应的是哪件事;三是干脆换块新表,但为了一个番茄钟换表实在不划算,而且这块表的硬件完全够用——它有屏幕、有震动、有按键、能连手机,缺的只是一个合适的软件。
于是我就动了念头:能不能自己给它写一个?正好这段时间我在用AI辅助编程工具做一些小项目,就想着拿这块旧手表练手。核心目标很明确:一个能在手表上独立运行的番茄钟+待办清单App,支持任务创建、番茄计时、震动提醒、完成打卡,最好还能跟手机端同步数据。关键词就三个:旧设备复用、番茄工作法、AI辅助开发。这篇文章我会把整个从零到跑通的完整过程拆开讲,包括我怎么选技术路线、怎么跟AI工具配合、踩了哪些坑、最后怎么把App装进手表里。如果你手里也有闲置的智能手表,或者想试试用AI coding做点小工具,这篇应该能给你不少参考。
2. 先搞清楚这块表到底能跑什么
2.1 华米GTR4的系统底子与开发入口
动手之前必须先摸清楚设备的软件底细,不然写出来的代码根本装不进去。Amazfit GTR4跑的是Zepp OS,这是华米自家的一套轻量级手表操作系统,跟Wear OS完全不是一回事。Zepp OS的应用生态相对封闭,但它提供了官方的开发者工具链,叫Zepp OS Developer Toolkit,支持用JavaScript写应用逻辑,界面则用一种叫UX的声明式框架来描述。这一点很关键——意味着我不需要去啃C++或者汇编,用前端那套思路就能上手。
我查了一下GTR4的硬件参数:屏幕是1.39英寸AMOLED,分辨率454×454,圆形表盘;处理器是低功耗方案,内存和存储都比较有限;有物理按键,支持震动马达,支持蓝牙连接手机。这些信息决定了我的App设计边界:界面必须适配圆形屏幕,不能做太复杂的动画,内存占用要克制,交互尽量靠按键和滑动完成。另外Zepp OS的应用是跑在手表本地的,但可以通过蓝牙跟手机上的Zepp App做数据通信,这就为后续同步留下了口子。
提示:不同型号的Zepp OS设备,API支持程度可能不一样。动手前一定要去官方开发者文档里确认你的具体型号支持哪些能力,尤其是震动、按键事件、本地存储这几项。
2.2 为什么选JavaScript而不是其他方案
Zepp OS官方主推的就是JavaScript开发,配套的还有一套叫Zepp OS Simulator的模拟器,可以在电脑上先跑起来看效果,不用每次都真机调试。我对比过几条路:一是用官方工具链老老实实写,二是找现成的开源番茄钟项目改,三是干脆放弃手表端只做手机端。第三条直接排除,因为我的核心诉求就是"不掏手机"。第二条的问题是现成的Zepp OS番茄钟项目很少,而且大多年久失修,改起来未必比从头写快。所以最终选了第一条:用官方JS工具链从零写。
选JS还有一个隐性好处——AI编程工具对JavaScript的支持是最成熟的。我用的AI coding助手在生成JS代码时准确率明显高于其他语言,尤其是涉及界面布局和事件绑定时,基本能一次成型。这大大降低了我的开发成本,毕竟我不是专业的手表开发者,很多Zepp OS特有的API我也是边查边用。
2.3 功能范围的取舍:先做减法
一开始我列了一大堆想做的功能:任务分类、优先级、标签、统计图表、云同步、多设备协同……但冷静下来想想,手表屏幕就这么大,交互方式就这几种,功能堆太多反而没人用。最后我砍到只剩四个核心能力:
- 任务管理:能新建、查看、完成、删除待办事项,每条任务有标题和状态。
- 番茄计时:选中一条任务后启动25分钟倒计时,结束时震动提醒。
- 打卡记录:每个完成的番茄自动记一笔,关联到对应任务。
- 本地持久化:数据存在手表本地,重启不丢。
同步功能我放到了二期,因为涉及手机端配合,复杂度陡增,先把单机跑通再说。这个取舍很关键——很多个人项目死在"想做的太多",先把最小可用版本跑起来,后面才有迭代的动力。
3. 用AI coding搭出第一版可运行骨架
3.1 跟AI工具配合的正确姿势
很多人用AI写代码的方式是丢一句"帮我写个番茄钟App"然后就等结果,这样出来的东西基本没法直接用。我的做法是把任务拆到足够细,每次只让AI解决一个具体问题。比如第一步我不是让它写整个App,而是问:"Zepp OS里怎么创建一个带文本和按钮的页面?给我一个最小示例。"拿到示例后我自己跑通,确认没问题,再问下一步:"怎么在这个页面上绑定按钮点击事件并修改文本?"这样一步步推进,每一段代码我都能理解、能验证。
另一个心得是给AI提供足够的上下文。Zepp OS的API跟标准Web API有差异,如果我不说明,AI很容易按浏览器的思路写,结果就是各种报错。所以我会在提问时附上官方文档里相关API的签名,或者直接说"这是Zepp OS环境,不是浏览器环境,请用Zepp OS的API"。加上这句之后,生成代码的可用率明显提升。
3.2 项目目录结构与核心文件
Zepp OS应用的标准结构大概是这样的:
my-pomodoro/ ├── app.json // 应用配置,声明权限、页面路由等 ├── app.js // 应用入口,生命周期管理 ├── page/ │ ├── home/ │ │ └── home.js // 主页:任务列表 │ └── timer/ │ └── timer.js // 计时页:番茄倒计时 └── utils/ └── storage.js // 本地存储封装app.json里要声明用到的权限,比如震动、存储。页面路由也要在这里注册,不然跳转会失败。我第一版就踩了个坑:忘了在app.json里注册timer页面,结果点击任务后跳转直接白屏,排查了半天才发现是路由没声明。
3.3 任务列表页的实现逻辑
主页的核心是一个可滚动的任务列表。Zepp OS的UI框架里,列表一般用SCROLL_LIST组件实现。每条任务渲染成一个列表项,显示标题和状态图标。数据从本地存储读取,用一个数组维护。
关键代码逻辑大致是这样(基于常见实践整理):
// 读取任务列表 const tasks = storage.getTasks() || []; // 构建列表数据 const listData = tasks.map((task, index) => ({ title: task.title, status: task.done ? '已完成' : '进行中', index: index })); // 绑定到SCROLL_LIST const list = document.getElementById('task-list'); list.setProperty(hmUI.prop.DATA, listData);点击某条任务时,把任务ID存到一个全局变量里,然后跳转到计时页。这里要注意Zepp OS的页面跳转是hmApp.gotoPage,参数传递不像Web那么方便,我最后是用全局变量+本地存储组合来传的。
3.4 番茄计时页与震动提醒
计时页的逻辑相对简单:进来后读取当前任务ID,启动一个25分钟的倒计时,每秒更新一次界面上的剩余时间。倒计时结束调用震动API,然后写一条打卡记录。
let remaining = 25 * 60; // 25分钟,单位秒 const timer = setInterval(() => { remaining--; updateDisplay(remaining); if (remaining <= 0) { clearInterval(timer); vibrate(); // 震动提醒 saveRecord(); // 记录打卡 hmApp.goBack(); // 返回主页 } }, 1000);震动用的是hmSensor或者hmSetting里的相关接口,具体哪个取决于系统版本。我实测下来,GTR4的震动强度够用,戴在手腕上能明显感觉到,不会漏掉提醒。
注意:
setInterval在手表息屏后可能会被系统挂起,导致计时不准。这是个很隐蔽的坑,后面我会专门讲怎么处理。
4. 那些让我卡了半天的坑与排查过程
4.1 息屏后计时停摆:最要命的一个问题
第一版跑起来后我兴冲冲地测试,结果发现一个致命问题:手表息屏之后,番茄钟就不走了。亮屏一看,时间还停在息屏那一刻。这意味着整个番茄钟根本没法用——谁专注的时候会一直亮着屏幕?
排查过程是这样的:我先怀疑是setInterval的问题,查了Zepp OS文档,发现系统为了省电,在应用进入后台或屏幕熄灭后会限制定时器执行。这不是bug,是设计如此。解决方案有两个方向:一是用系统提供的"后台定时"能力,二是改用时间戳计算而不是依赖定时器累加。
我最终选了第二种,因为它更可靠。具体做法是:启动番茄钟时记录一个开始时间戳,每次界面刷新时用"当前时间 - 开始时间"算出已经过去多久,而不是靠每秒减一。这样即使定时器被挂起,只要界面重新激活,算出来的剩余时间依然是准确的。震动提醒则配合系统的后台唤醒能力,在预计结束时间点触发。
const startTime = Date.now(); const totalMs = 25 * 60 * 1000; function getRemaining() { const elapsed = Date.now() - startTime; return Math.max(0, totalMs - elapsed); }这个改动之后,息屏再亮屏,时间显示完全正确。这个坑我踩了差不多一个下午,但想通之后觉得特别值——它让我理解了嵌入式设备上"时间"这件事不能想当然。
4.2 本地存储的读写时机与数据丢失
第二个坑是数据丢失。有几次我新建了任务,重启手表后任务就没了。查下来是存储写入的时机不对——我原本是在页面销毁时才写存储,但Zepp OS的页面销毁时机不一定可靠,有时候应用被系统直接杀掉,销毁回调根本没执行。
改成每次数据变更后立即写入,问题就解决了。虽然写入频率高了,但任务数据量很小,对性能没影响。存储用的是hmFS或者localStorage风格的接口,具体看系统版本。我封装了一个storage.js,统一处理序列化和反序列化,避免到处写重复代码。
// storage.js export function saveTasks(tasks) { localStorage.setItem('tasks', JSON.stringify(tasks)); } export function getTasks() { const raw = localStorage.getItem('tasks'); return raw ? JSON.parse(raw) : []; }4.3 圆形屏幕的布局适配
GTR4是圆形表盘,我第一版界面是按方形思路做的,结果四个角的内容全被裁掉了。Zepp OS的UI框架虽然会自动处理一部分适配,但边距和元素位置还是得自己调。
我的做法是:所有内容都限制在一个内切圆范围内,关键信息放在屏幕中央区域,边缘只放不重要的装饰。列表项的高度也调小了,保证一屏能显示足够多的条目。另外圆形屏幕上的文字对齐要特别注意,居中对齐比左对齐更稳妥,因为左右边缘的可用宽度是变化的。
4.4 真机调试与模拟器的差异
模拟器上跑得好好的,装到真机上却出现各种问题:字体渲染不一样、震动没反应、按键事件不触发。这是因为模拟器毕竟不是真实硬件,很多传感器和外设行为它模拟不了。
我的经验是:模拟器只用来验证逻辑和大致布局,真机调试才是关键。Zepp OS提供了真机调试的桥接工具,可以把应用直接推到手表上运行,还能看日志。虽然每次部署要等一会儿,但比在模拟器上自欺欺人强得多。尤其是震动、按键、息屏这些跟硬件强相关的功能,必须真机验证。
5. 让番茄钟真正好用的几个细节打磨
5.1 按键交互:减少抬手滑动
手表上滑动操作其实挺累的,尤其是运动或者手上有东西的时候。GTR4有物理按键,我就把最常用的操作映射到了按键上:短按开始/暂停计时,长按结束当前番茄。这样即使不看屏幕,盲操作也能控制计时,体验提升非常明显。
实现上要监听按键事件,Zepp OS提供了onKey之类的回调。注意不同按键的键值不一样,要查文档确认。我一开始把键值搞错了,按半天没反应,后来打印日志才找到正确的值。
5.2 任务与番茄的绑定关系设计
一开始我把任务和番茄记录分开存,结果统计的时候对不上——不知道哪个番茄属于哪个任务。后来改成每条番茄记录里带上任务ID,任务本身只存标题和状态,统计时通过ID关联。这样数据结构更清晰,也方便后续做"每个任务花了几个番茄"的统计。
// 番茄记录结构 { taskId: 'task_001', startTime: 1699999999999, duration: 25 * 60 * 1000, completed: true }5.3 默认时长可配置
25分钟是番茄工作法的经典时长,但不是所有人都适用。我加了一个设置项,允许在20到45分钟之间调整。设置存在本地,启动计时时读取。这个功能实现起来很简单,但实用性提升很大——我有个朋友习惯用45分钟一个周期,没有这个配置他就用不了。
5.4 完成打卡的正反馈
番茄钟结束后的反馈很重要。我除了震动,还在屏幕上显示一个简单的完成动画和"第N个番茄"的提示。别小看这个,它给了用户一种积累感,坚持用下去的动力会强很多。动画不用复杂,一个缩放或者淡入淡出就够了,手表性能有限,太花哨反而卡。
6. 从单机到同步:数据打通思路
6.1 手表与手机通信的基本原理
Zepp OS应用可以通过蓝牙跟手机上的Zepp App通信,官方提供了消息通道API。基本模式是:手表端发送消息,手机端有个配套的服务接收并处理。但这里有个限制——手机端需要有一个能接收消息的载体,要么是Zepp App的小程序,要么是配套的原生应用。纯手表端应用没法直接跟云端通信。
我评估了一下,做完整的双向同步工作量不小,涉及手机端开发、消息协议设计、冲突处理等。所以二期我只做了单向的"手表推送到手机",把番茄记录同步过去,手机端负责展示和备份。反向的任务下发暂时没做,因为手表上直接新建任务已经够方便了。
6.2 消息格式与容错
通信消息我用JSON格式,包含类型、时间戳、数据体三个字段。发送前先检查蓝牙连接状态,没连接就暂存到本地队列,等连接恢复后补发。这个队列机制很有必要,不然手表离开手机范围时产生的记录就丢了。
function sendRecord(record) { if (!isConnected()) { pendingQueue.push(record); return; } const msg = { type: 'pomodoro_record', timestamp: Date.now(), data: record }; sendMessage(JSON.stringify(msg)); }容错方面,接收端要做数据校验,防止格式错误导致崩溃。我吃过这个亏——有一次发了个字段缺失的消息,手机端直接报错,排查了半天。
7. 关于旧设备复用和AI辅助开发的一点个人体会
这块GTR4现在成了我每天用得最多的设备之一,不是因为它有多先进,而是因为它上面跑着一个完全按我需求定制的工具。整个项目从起念到跑通大概花了两周多的业余时间,其中真正写代码的时间可能只占一半,剩下都在查文档、调试、踩坑。但这个过程让我对"旧设备复用"这件事有了新的认识:很多被淘汰的硬件,性能其实远没到不能用,缺的只是匹配的软件。而AI辅助编程工具的出现,恰好把"写软件"这件事的门槛降到了普通人能够到的程度。
我用AI coding的过程中最大的感受是:它擅长的是"填空",不擅长"架构"。你得先想清楚要做什么、怎么拆、用什么技术路线,然后让AI帮你把每一块的具体实现写出来。如果你自己都没想明白,指望AI给你一个完整方案,结果往往是一堆跑不起来的代码。所以人的判断力依然是核心,AI只是把执行效率放大了。
另外一点是关于"够用就好"的心态。我见过太多个人项目因为追求完美而永远停留在"开发中"。这个番茄钟现在还有一堆可以优化的地方——界面不够精致、没有统计图表、同步只做了单向——但它已经能稳定地帮我完成每天的专注任务了。先让它跑起来,再慢慢打磨,这个顺序不能反。
如果你也想拿手里的旧设备做点什么,我的建议是从一个你每天都会用的小功能开始,别贪大。手表、旧手机、旧平板,甚至树莓派,都是很好的练手平台。工具链现在都很成熟,AI也能帮上大忙,真正稀缺的是"我想解决这个问题"的那股劲。