词迹:背单词 App
一个从自己背词需求出发做的个人工具:用 Flutter、SQLite 和记忆曲线管理新词与复习,后来因为 iOS 免费签名限制,又把它迁成了能直接在手机浏览器使用的网站版。
这个项目不是从“我想做一个背单词软件”开始的,而是从一个很实际的问题开始:我想按自己的习惯背词,但现成的软件总有一些地方用得不顺手。
我想要的东西其实很具体。单词可以自己批量加进去,可以分组;第一次背过以后,不应该第二天又和没见过一样随机出现;答错的词要多出现,连续答对几次以后又能从易错词里恢复;当天该复习的词最好和额外练习分开,不然很容易为了完成数量,反而把真正到期的词漏掉。
所以我用 Flutter 做了一个给自己使用的背单词 App。后来因为 iOS 免费签名到期,手机里的 App 会直接变成“不可用”,我又把它迁成了网站版。这个项目最后解决的已经不只是“怎么背单词”,还有“一个自己做的工具怎样才能真的长期用下去”。
第一步:建立完整的背词流程
第一版没有先做复杂界面,我给自己定的主流程是:
- 手动或批量添加英文单词与中文意思。
- 开始练习,看到英文后输入中文。
- 系统先根据答案做一个简单判断。
- 我自己确认判对或判错。
- 把这次结果写回单词状态,决定它下一次什么时候出现。
我没有让自动判题直接替我做最后决定。中文释义很难只靠字符串做到完全准确,比如“允许”和“准许”意思很接近,写法却不同;有些单词又有多个释义。程序会把逗号、分号、顿号等分隔符统一处理,再判断是否命中主要释义,给出“可能正确”“需要确认”或“可能错误”,最后仍由我手动确认。
现在看,这个做法不算很智能,但它比较诚实。与其让程序用一个看起来确定的结果误导我,不如把它定位成辅助判断。
第二步:把随机练习变成到期复习
每个单词不只保存英文和中文,还会记录状态、答对次数、答错次数、连续答对次数、复习等级和最后复习时间。App 不会提前把下一次复习日期写死,而是根据 最后复习时间 + 当前复习等级 动态计算。
我设置的间隔依次是当天、1 天、2 天、4 天、7 天、15 天、30 天和 60 天。答对以后等级上升,答错以后等级下降,并进入易错词;连续答对 3 次后,错误次数清零,易错词回到旧词。新词练过几次以后也会自动进入旧词。
出题时,我又把学习分成了三个入口:
- 背新词:只从从未背过的词中抽取。
- 今日复习:只显示按照复习间隔已经到期的词,优先把当天应该完成的内容做完。
- 继续复习:当天到期词结束以后,再按照设置从易错词、旧词和已经学过的新词里补充练习。
如果某个词属于一个小组,只要它被抽中,同组中符合当前模式的单词也会一起出现。我做这个规则,是因为有些近义词、固定搭配或同一课的单词,本来就更适合放在一起比较着记。
学习数据:真正需要保存的是过程
App 使用 SQLite 保存数据,一共包括单词、单词小组、出题设置和学习记录四部分。重复导入同一个单词时,不会覆盖已经积累的复习进度,而是合并新的中文释义。删除小组时,组内单词也不会被一起删除,只会解除分组。
每次学习结束后,程序会记录学习或复习模式、开始和结束时间、完成数量、正确数与错误数。统计页面可以按天、周、月和年查看学习次数、完成词数、正确情况与投入时间。
这部分让我感觉比较明显的一点是:数据库结构不只是为了把数据存下来,它其实直接影响使用体验。重复导入时保不保留学习记录,小组删除时怎么处理单词,复习日期是保存还是计算,都是产品规则。
单词录入:减少重复输入才愿意长期使用
如果每个词都要手动输入,真正使用时很容易嫌麻烦。所以我做了图片导入。App 可以从相册选择图片或直接拍照,先用 Google ML Kit 做离线文字识别,适合比较清楚的印刷体;手写词表则可以交给电脑上的 FastAPI 服务,再通过视觉模型识别英文和对应的中文意思。
这里最麻烦的不是“把图片转成文字”,而是把左右两栏重新配成一行一组。英文在左、中文在右时,识别结果的顺序不一定正好对应;手写体、网格线和词性标记也会影响结果。
所以无论使用离线 OCR 还是云端识别,结果都不会直接写进数据库。程序会先生成一份草稿,我可以修改英文、中文和所属小组,删除识别错的行,最后再确认保存。AI 在这里负责减少录入工作,而不是替我决定最终数据。
从 App 可用到能够长期使用
Flutter 让我可以同时准备 Android 和 iOS 版本,但“能在 iPhone 上运行”和“能够一直使用”并不是一回事。我最初通过 Xcode 和普通 Apple ID 把 App 安装到手机。免费开发签名到期以后,点击图标会直接提示 App 不可用,需要重新连接 Mac、重新签名和安装。
加入 Apple Developer Program 可以缓解这个问题,但对一个主要给自己用的工具来说,每年付费、处理签名和上架流程并不轻。代码已经写好了,数据和复习逻辑也能正常工作,最后却因为分发方式导致每天使用很麻烦。
这时我才真正意识到:一个工具能不能被方便地打开,也是功能的一部分。
网站版“词迹”:换一种更适合自己的交付方式
后来我把这个 App 迁成了适合手机浏览器使用的网站,并取名为“词迹”。网站版保留了批量加词、单词本、分组、背新词、到期复习、易错词、发音、统计和出题设置,数据从手机本地 SQLite 改为云端 D1 数据库。
在 iPhone Safari 中可以把网站添加到主屏幕,使用时不需要每隔几天重新签名,也不需要电脑一直开着。图片导入则调整为配合 iPhone 的实况文本,把识别出的内容复制到批量导入区域,避免为了 OCR 再维护一台本地服务器。
从 App 到网站不是简单地把页面重新写一遍。原来默认只有一个人、一台手机、本地数据库;迁到网站以后,要重新考虑云端数据、访问权限、手机浏览器操作和数据备份。但它让这个工具终于从“我做出来了”,变成了“我随时都能用”。
我的心路历程:做出来和真正用起来
这个项目的技术并不算特别复杂,自动判题仍然只是基于释义文本的启发式判断,复习间隔也是我自己设定的一套固定规则;App 版的云端 OCR 依赖电脑服务和网络,网站版也没有完全复刻所有原生能力。
但我还是很喜欢这个项目。它不是为了展示某个框架而做的,而是我自己真的有这个需求,才一点点把数据管理、复习规则、OCR、发音、统计和跨平台使用补起来。后来遇到 iOS 签名问题,我也没有让项目停在“代码能跑”的阶段,而是换了一种更适合自己的交付方式。
它让我学到的一件事是:做个人工具时,最重要的不一定是功能最多,而是它能不能进入自己的日常。一个每天都很难打开的 App,再完整也很难真正产生价值。
用到的技术
- Flutter
- Dart
- SQLite
- sqflite
- Google ML Kit
- Flutter TTS
- FastAPI
- Python
- Vision Model
- Next.js
- Cloudflare D1
Flutter Dart
完成 Android、iOS 共用的界面和背词流程,包括首页、单词本、分组、答题、设置与统计。
SQLite sqflite
在手机本地保存单词、分组、复习等级、答题次数和学习记录,让复习进度不依赖网络。
Google ML Kit
提供离线文字识别,主要处理印刷体或比较清楚的词表图片。
FastAPI Python 视觉模型
组成手写词表识别服务,把图片中的英文和中文释义配对,再返回 App 进入人工确认页面。
Flutter TTS
在切换单词时自动朗读英文,也保留手动播放按钮。
Next.js Cloudflare D1
用于后来的“词迹”网站版,把原本只保存在一台手机里的数据迁到云端,并适配手机浏览器使用。