
1. 一场婚礼让我意识到礼金这笔账脑子真记不住去年十月参加一位老同事的婚礼婚宴大厅门口摆着一张签到台上面放着一本大红礼簿来客要在纸上写下名字和金额。我当时帮着他家里人收了一会儿礼金发现场面比想象中混乱礼簿、手机备忘录、群里的转账记录、红包背面写的字各种信息混在一起晚上清点时对了三遍才理清楚。婚礼结束后我问同事这本礼簿以后怎么查。他说锁抽屉里以后谁家办事儿的时候翻出来看看。我又问真翻得出来吗他笑了笑说实话基本不会翻。到时候全靠记性。这个回答让我心里咯噔一下。礼金这个东西在中国的人情社会里不只是钱更是一笔需要长期维护的人情账。你今天收了别人800块改天人家办满月酒、办乔迁、办升学宴你回礼至少得对等关系好一点的也许还要加上一些。一年里人情往来几十次全靠脑子记半年之后基本就是一笔糊涂账。长辈们习惯用纸质礼簿但纸质礼簿只能按时间顺序翻想查某个人给过多少几乎不可能想算今年收了多少、送出去多少更是要靠手动加。用通用记账App里面全是餐饮、交通、购物这些分类搜一个人名要在几千条流水里翻找而且大部分记账App带广告、带理财推广用起来实在心累。于是我决定自己做一个安卓应用专门记礼金而且要满足三个条件免费、无广告、好用。这个项目前前后后做了大约四个月从需求梳理到上架发布踩了不少坑也收获了很多真实反馈。这篇文章把整个项目从头到尾复盘一遍功能设计、技术选型、上架运营的实操细节都会写到希望能给同样想做个人工具类App的朋友一些参考。2. 功能设计阶段砍掉一半想做的功能2.1 从真实场景倒推核心需求做工具类App最大的误区就是一上来想把所有功能都塞进去。我第一版规划里写过密码锁、云同步、账单图表、回礼提醒、多账本、主题换肤……后来冷静下来重新从用户拿到这个App的三秒钟里要干什么出发把所有功能重新过了一遍。真实场景其实特别简单。某人参加婚礼回来掏出手机打开App输入王叔800婚礼点保存。过阵子自己孩子满月收了一堆红包挨个记下来。年底想看看今年人情往来的总体情况点一下统计。等别人也办酒席时搜一下对方名字看看当年给过多少心里有个底再决定回多少。从这个流程反推核心需求只有四个快速记账、按人查历史、按时间段统计、数据不丢失。其他都属于锦上添花。云同步本地备份导出也能实现数据不丢。密码锁真正想偷看你手机的人少之又少而且系统本身也有应用锁功能。主题换肤工具类应用最不值钱的就是皮肤。于是这些全部砍掉只留下最核心的闭环。2.2 数据模型一张表就够起步整个App的数据模型极其简单核心就是一张表我整理了一下字段字段类型说明idLong主键自增person_nameString对方姓名或称呼amountLong金额单位为分避免浮点误差typeInt礼金类型收礼 / 送礼categoryString事由婚礼、满月、乔迁、生日、升学、其他event_dateLong事件日期存毫秒时间戳remarkString备注比如对方是否带家属、关系亲疏created_atLong创建时间金额用分存储是记账类App的基本常识。如果用Double存元0.1 0.2这类浮点运算问题会给你带来源源不断的麻烦显示层四舍五入也容易出偏差。用Long存分SQL的SUM直接累加结果精确到分展示时除以100就行。这个细节虽然老生常谈但真正踩过这个坑的人都知道它绝不是小事。为什么要有type字段因为收礼和送礼是两种完全不同的操作。收礼是别人给你的你需要记住对方的人情送礼是你给别人的你需要惦着将来找个时候还回去。两者都是重要的人情账但统计口径不同分开存才能在统计时给出准确的总览。2.3 明确不做的功能清单我把不做的功能列了一份清单贴在开发计划的第一页不做登录注册。本地单机应用不需要账号体系没有账号就从根本上杜绝了用户数据上传服务器的可能。不做云同步。第一版不做理由后面细说。不做导购、理财推荐、广告信息流。这类东西是很多记账App的盈利来源但也是用户卸载的原因。不做多账本。第一版一个账本足够多账本会让数据模型复杂一倍。不做语音记账。识别准确率不够高修正识别错误的成本大于手动输入。这个阶段最深的体会是功能边界不是靠克制硬憋出来的而是靠想清楚用户为什么要用你而不是用别人。你的工具解决一个痛点解决到极致就有存在的价值。什么都做等于什么都没做。3. 技术选型原生Kotlin Compose Room为什么这么选3.1 为什么新项目一律用Compose这个项目的技术栈是原生Kotlin加Jetpack Compose加Room没有引入复杂框架。刚立项时我也犹豫过要不要用跨平台方案毕竟一套代码以后还能出别的版本。后来想清楚了目标用户就是安卓用户别的版本短期内根本不在考虑范围内跨平台的红利我吃不到反而要承担包体偏大、启动效率略低的问题。原生Compose应用安装包体积小、启动快、内存占用低这在低端安卓机上的区别还是挺明显的。Compose的开发体验确实比传统XML布局舒服太多。声明式UI写起来直观状态变了界面自动更新不用手动去操作视图再逐个更新文本。我对这个项目的要求是单页面快速录入Compose的状态模型天然适合这种场景输入一个字段界面上实时显示汇总数字逻辑非常自然。举个例子录入页面的底部会实时显示当前账本累计收礼XX元送礼XX元这个数字随每次保存自动刷新。用Compose写这段逻辑只需要一个可观察的状态对象保存时更新状态界面自动重绘几乎不用额外的胶水代码。3.2 数据库Room比裸写SQLite省心太多数据层我选了Room。Room把SQLite的样板代码降到了最低建表、写数据访问接口、定义查询全部是编译期校验的注解和接口。比如按人查询历史记录和按时间范围算总额就是两个普通的数据访问方法Dao interface GiftRecordDao { Query(SELECT * FROM gift_records WHERE person_name LIKE :keyword ORDER BY event_date DESC) suspend fun searchByPerson(keyword: String): ListGiftRecord Query(SELECT SUM(amount) FROM gift_records WHERE type :type AND event_date BETWEEN :start AND :end) suspend fun sumByTypeAndRange(type: Int, start: Long, end: Long): Long? }Room会在编译期帮你校验SQL语法和返回类型表结构改了之后还能通过迁移机制保留老用户的数据。对个人项目来说这一点价值巨大——我后面改过一次表结构加了事由分类字段Room的Migration机制让老用户升级不丢数据这个省下的功夫远比引入一个更轻量的数据库方案多得多。3.3 协程与Flow列表刷新的正确姿势数据量小不代表不需要合理的响应式架构。我用协程加Flow实现了首页最近流水列表的自动刷新——每次保存新记录时表里的数据变了Flow会自动重新发射界面立刻更新。这对用户体验的提升是直接且明显的保存完一条记录不用手动回首页刷新列表里已经能看到最上面的新记录了。Flow在这个场景下的原理很简单数据库查询被包装成一个可观察的数据流表数据变动时Flow会重新执行查询并把结果传给订阅者。Room原生支持把数据访问方法声明成Flow返回类型写起来一行都不用多。这也是我建议即使小项目也别用裸SQLite的原因之一——用裸SQLite实现同样的数据变动自动通知界面效果需要内容观察器之类的手动机制Room里一个类型声明就解决了。4. 免费无广告不是口号是一整套设计决策4.1 先砍掉联网权限从源头杜绝广告免费无广告这句话说起来容易做起来需要一整套技术决策托底其中最关键的一条是整个App不申请网络权限。我故意在应用配置文件里完全拿掉网络权限。广告SDK首先要联网拉取广告和上报请求你连网络权限都不声明广告SDK就算接进来也无法正常工作。所以这个决策从根上保证了无广告、无追踪不是一句宣传语而是用户可以自己验证的事实——在系统应用权限详情页里任何人都能看到这个App没有联网权限。有人会说很多广告SDK可以通过组件间通信或者借用其他应用的方式间接联网。对确实存在这种激进做法但那是恶意应用的问题不是我一个普通工具App的考虑范围。我的目标很简单给用户一个清清楚楚的权限列表。没有网络、没有定位、没有通讯录、没有存储干干净净。4.2 导出备份用系统文件选择器替代存储权限数据备份怎么实现如果走传统路线需要申请存储权限去读写存储区域这个权限又敏感又容易惹争议。我的方案是导出时用系统的文件选择器让用户选保存位置导入时用系统的文件选择器选文件全程不申请任何存储权限。具体实现走的是系统存储访问框架的标准思路调起创建文件的系统操作来生成目标文件调起打开文件的系统操作来读取导入文件。系统会让用户自己决定文件放在哪里应用只拿到一个文件描述符读写完之后即关闭。老用户升级不会碰到权限弹窗新用户从应用详情页看到的权限列表永远是空的。这个方案唯一的小要求是代码运行时需要用户做一个选位置的动作对备份这种低频操作来说完全不算负担。4.3 广告SDK的成本比想象中高这个项目从一开始就不打算接广告但我也认真评估过广告SDK的成本结论是对个人开发者来说广告SDK的隐形成本远比那点收入高。首先广告SDK会明显增加安装包体积一个主流广告SDK几十MB很常见小应用装上之后用户下载的耐心就少了一截。其次广告SDK的初始化通常会占用启动时间广告位加载失败还要处理各种回调异常处理做得不好应用就爱崩。更麻烦的是合规要求主流广告SDK在隐私介绍、用户授权、个人信息清单这些方面都有严格要求个人开发者一旦某个细节没做到位应用商店的审核就会卡住。为了赚那点广告费要搭进去远超预期的开发、测试和沟通时间这笔账怎么算都不划算。所以这个项目最终选了一条更简单的路不接广告、不用服务器、不收集任何数据把全部精力放在把记账这件事做顺上。没有商业压力界面干净弹窗少交互效率高用户自然留得住。4.4 隐私页面怎么写与其说一套做一套不如直接做一套免说上架应用商店需要填写隐私说明。很多开发者的做法是抄一份通用模板写得面面俱到但实际都没做到。我的做法相反先把不做什么写清楚。隐私说明里有一句我特意加粗了本应用不收集、不传输任何用户数据不集成任何第三方SDK。这句话每个开发者都可以写但如果你声明了联网权限、接了统计SDK就不能这么写。因为我的应用真的什么都没有所以这句话经得起核查。我在商店展示页也会放上应用权限截图用户看到权限列表几乎是空的下载意愿会比看到一堆权限高很多。信任不是靠文案建立的是靠技术选择保障的。从立项第一天就没有联网权限这就是对用户最好的承诺。5. 从开发到上架踩过的坑和填坑方案5.1 金额输入框的键盘类型陷阱第一个坑来自一个很小的细节金额输入框的输入法键盘类型。我在输入框配置里设置了数字键盘但实测发现部分输入法在部分机型上依然会弹出全键盘。用户输入800的时候全键盘上数字键分布不一样容易误触而且很多输入法的全键盘默认会打字母一不小心就变成一串乱码。解决思路是双保险输入框的类型设置加上小数支持同时在输入回调里做过滤非数字字符一律吞掉。Compose里可以用键盘配置配一个过滤逻辑val filtered text.filter { it.isDigit() || it . } if (filtered ! text) { inputText filtered }这个过滤加上之后不管输入法怎么捣乱输入框里能留下的只有数字和小数点。另外还要处理一个小细节用户输入整数800时不要自动补成800.00显示出来占位太宽容易让人看错。我的处理是金额字段在输入时显示用户原始输入失焦后如果只有一位小数就补零比如800.5显示为800.50。这个处理在体验上不起眼但记账场景里几乎天天用它。5.2 数据库迁移第二个版本就遇到了第一版上线之后有用户反馈说希望能记录是婚礼还是满月这确实是合理需求。于是我在表里加了一个category字段。这里的坑不是加字段本身而是老用户升级时数据库版本变化的处理。如果不处理升级后的应用会直接崩溃用户数据全部丢失这对工具类应用来说就是灾难。用Room写迁移很简单在数据库构建配置里加上迁移对象val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE gift_records ADD COLUMN category TEXT NOT NULL DEFAULT 其他) } }但真正坑人的是如果第一版发布时就没给字段设计默认值迁移脚本里就必须处理存量数据的空值。我那次因为加了非空默认值所以不用回填但后来测试时我故意去掉默认值升级直接报错。建议所有人在第一版建表时就养成写迁移脚本的习惯不要以为以后不会改表结构。只要产品还在迭代表结构就一定会变。5.3 CSV导出乱码一个编码标记的战争导出功能上线后有用户反馈在电脑上打开导出的文件中文全部变成乱码。这个问题的原因在技术上很简单导出文件是UTF-8编码而电脑上常见的表格软件默认按本地编码解析没有编码标记的UTF-8文件会被当成本地编码读取中文自然乱码。解决方案也很简单写出文件时在开头加上UTF-8的编码标记即三个字节EF BB BF表格软件就能正确识别为UTF-8了。outputStream.write(byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()))这个三行代码的问题让我来回折腾了一天。一开始我以为是编码转换的问题把流里的字符串反复转码试了十几次都不对后来才意识到是文件头标记的问题。这类小问题大折腾在个人开发里特别常见——不是你能力不行是你把时间花在了错误的方向上。如果你也遇到类似情况先确定解析端是什么软件、什么系统、什么编码习惯再决定怎么输出比盲猜编码高效得多。后来我还加了导出格式选项带编码标记的版本表格软件友好和纯UTF-8版本通用用户按需选择。这个选项一加乱码类反馈确实少了很多。5.4 签名密钥丢失是最可怕的事故安卓应用的签名密钥丢失意味着一个严重的问题你永远无法更新这个应用。因为商店上架的应用包和本地新构建的包签名不一致时新版本无法覆盖安装用户只能卸载重装数据全部丢失这在用户眼里等于应用废了。我差点经历这个事故。有一次电脑系统重装我同步文件时把密钥文件漏掉了幸好备份里还有一份旧的赶紧找了回来。把这件事写出来是想提醒所有做安卓开发的人密钥文件的备份建议至少存在三个地方——本地磁盘、云盘、加密U盘。密码也单独记在一个加密笔记里不要和密钥文件放在同一处。这是一个平时完全用不上、一旦丢了就后悔莫及的容灾细节。另外建议把签名信息独立成配置文件不要硬编码在构建脚本里这样即使项目源码流传出去签名信息也不会泄露。签名文件丢失或者泄露都可能让应用被人改动重打包这个问题比大多数人想象中严重。5.5 上架前的合规检查清单上架应用商店这件事很多第一次做安卓个人开发的同行会忽略一些细节。我把自己的检查清单整理出来照着做基本不会被卡目标系统版本要达到应用商店当前要求的最低标准版本过低会被直接拒绝。隐私说明要有且内容必须与实际行为一致包括是否收集信息、是否使用第三方SDK。应用图标和名称要避免夸大宣传不能暗示与任何知名品牌存在关联。权限声明要和使用场景匹配每个权限都要能说清楚用在哪个功能上。如果应用有登录功能审核人员需要能进入应用测试路径必须可用。因为我的应用不联网、不申请任何权限上架审核非常顺利从提审到通过只用了不到两天。这也是最小权限设计带来的额外红利——审核团队每天看几十个申请了一堆权限的应用突然看到一个权限列表几乎全空的通过概率自然就高了。6. 上线三个月用户教我的三件事6.1 按人查历史是刚需查询入口要够快上线后收到最多的反馈不是加个新功能而是按人查历史这个操作要做得更快。我第一版做了搜索框用户输入名字再点搜索出来一个列表。用户觉得这个流程太慢先点搜索框再输名字再点搜索至少要三步。我后来加了最近联系的人快捷入口——在首页快速录入区下方直接展示最近记过的十个人名点一下名字就直接跳到这个人的人情历史页。这个改动让查某个人给过我多少从三步变成一步。上线后这个入口的使用频率很高几乎占到所有查询操作的一半以上。这说明在记账工具里用户最频繁的行为其实不是记而是查。做工具的同行可以记住这个规律高频操作永远值得多做一层优化。6.2 回礼参考其实是隐藏需求统计功能上线后我加了一个人情总览页面显示收礼总额、送礼总额、净额。有用户留言说能不能在某个人的详情页里直接显示当年他给我送过多少、我给过他多少这样回礼的时候心里才有个数。这个需求其实早就存在但我第一版没有识别出来。它的本质是每一个人名详情页都应该自带一个人情往来汇总展示双方历年往来的总体情况。这个功能我很快加上了。实现上就是按人名做分组聚合查询把对应人所有的收礼、送礼记录各取总额和笔数展示在页面顶部。这个功能上线后应用商店的评分又涨了一截。我把它写在这里是想说很多时候用户不会直接说出真正想要的东西你要从他的话语里翻译出真实需求。回礼的时候心里有数翻译过来就是给我一个按人的往来汇总。6.3 有些人想要云同步但我不想做——以及原因云同步是用户呼声比较高的功能。我在商店问答区解释了为什么第一版不做本地单机加系统备份已经算数据不容易丢的及格方案而云同步需要服务器、账号系统、同步协议成本和维护精力都消耗巨大更重要的是一旦引入账号体系就与无广告零权限的产品定位产生冲突。为了满足一部分用户的需求把大多数用户最看重的隐私性和轻量性牺牲掉不符合这个工具的定位。我可以接受的折中方案是适配系统云备份机制。系统自带的自动备份会把应用私有数据备份到用户自己的云端换手机时理论上能恢复但不同厂家实现的可靠性参差不齐我只能把它作为兜底不承诺百分之百恢复。同时应用内明确提示用户定期导出备份文件文件存在本地还是存到自己信任的网盘完全由用户决定数据始终掌握在用户自己手里。7. 上线半年后的反思与后续计划7.1 维护一个免费应用的真实时间成本很多人以为上架就算结束其实对工具类应用来说真正的投入才刚刚开始。这半年我平均每周要花三到四个小时处理反馈、修复问题、优化细节。有的反馈是崩溃日志需要在特定机型上复现有的反馈是设计建议需要判断是不是伪需求还有的是纯粹的使用疑问比如怎么把数据导到我新手机上这类问题回复一次下次直接在应用内加个常见问题入口就能减少大量重复劳动。每一次小版本更新也都有隐形成本。发布前要重新走签名、打包、测试、提交的完整流程审核等待期间还要提心吊胆。所以现在我会把零散的改进攒起来凑够一个批次再发版。这样用户的版本更新频率不会太过频繁我的维护压力也控制在可承受范围内。免费应用不代表零成本你只是把收入预期换成了口碑和成就感。7.2 接下来想做的方向基于这半年的反馈我下一步的优化重点仍然是按人维度的体验。比如把某个人的所有记录做成年份维度的时间轴视图一眼看清和这个人多年来的礼尚往来走势数据导出格式再细分支持合并同类的汇总导出还有就是录入的智能化比如常用事由记住上次选择、同一人名自动带上之前的分类和备注让第二次记录同一个人的时候几乎不用再花心思。这些方向都不会引入联网权限也不会动摇免费无广告的定位。功能可以慢慢扩底线不动。我越来越觉得一个小工具的长期生存能力不在功能多少而在用户是不是每次打开都觉得顺手、放心。7.3 给想做同类工具的人的建议如果你也正犹豫要不要做一个解决自己身边小问题的工具应用我给的建议是别犹豫就做。工具类应用的竞争没有想象中激烈大量同类产品都是半死不活的状态你只要把一个核心点做到位坚持免费、干净、无广告用户会自己帮你传播。但前提是想清楚你的产品边界别在需求膨胀里把自己淹没了。先上线一个最小的可用版本把一条最核心的路径跑通让真实用户用起来再根据反馈迭代。第一版做出来之后你会听到很多声音有的声音会让你怀疑方向有的声音会纠正你真正该走的路。多听那些提出了具体场景、具体困扰的反馈少听那些泛泛而谈的能不能更炫酷。这个项目真正让我意外的收获是开发之外的东西。做之前我以为这是一个技术问题做完了才发现技术从来不是瓶颈对用户场景的理解才是。从这个角度看做一个礼金记账本和做一个任何领域的工具底层逻辑都是一样的找到一个被忽视的小痛点用一个干净、免费、专注的方案把它解决掉然后坚持下去就够了。