乐于分享
好东西不私藏

我用半天做了个「悬浮球录音」App,踩过的坑比代码还长

我用半天做了个「悬浮球录音」App,踩过的坑比代码还长

📖 全文约 1964 字 | 阅读约 5 分钟

我有一个很日常但很折磨人的痛点——看书、刷文章、走路通勤的时候,脑子经常蹦出一些想法。等我掏出手机,打开备忘录,开输入法,几个字打一半,想法已经跑了。

所以我就想要个极简的东西:桌面上一个悬浮球,点一下开始录音,再点一下停止。录完自动把语音转成文字,按日期归档。每天的灵感文字合并成一页笔记,想复制的时候一键拷走。

听起来很简单对吧?

做起来花了半天。

第一阶段:把录音跑起来(「能用」就行)

第一版很快跑通了——Android 的 !!MediaRecorder!! API 其实很稳。录是能录,但问题马上就来了:

问题 1:悬浮球不圆,是个橄榄球 🏉

用 XML 的 layer-list 叠形状,渲染出来椭圆了。后来干脆改最简单的 oval 单层,外面容器锁死正方形,搞定。

问题 2:录下来的文件打不开,播放器提示「媒体错误」

Debug 一圈发现是文件名没带后缀,MIME 类型也写错了。我以为随便扔个路径就行,结果 Android 媒体服务不认。统一改成 ==.m4a + audio/m4a==,终于能播了。

问题 3:录音文件没存到系统录音目录

原代码写死了一个私有目录路径。改成公共 Music/InspirationRecorder,再调一下 MediaStore 让系统扫一圈——文件管理器也能看到了。

这一阶段差不多半天。说真的——==功能越简单,环境细节越容易卡人==。

第二阶段:接入豆包语音识别(噩梦开始)

这个 App 最值钱的就是「语音转文字」。我接的是火山引擎!!豆包!!的语音识别 API。

问题 4:接口 URL 怎么调都不对

官方文档新版旧版参数和请求头完全不搭。一开始把 API Key 直接塞 X-Api-Key,返回 401。Debug 到后面才反应过来——我用的是 ==豆包大模型的 Key,根本不是语音识别的 Key==,两条产品线,两套东西。

问题 5:文件托管连环翻车(卡我最久的一块)

豆包 ASR 需要你给一个公网直链让它下载音频。也就是说,我先得把录好的 m4a 传到某个公网服务器,拿到 URL 再发给豆包。

我试了多少个服务?数一下:

❌ catbox.moe → stream was reset: PROTOCOL_ERROR(HTTP/2 握手失败)
❌ tmpfiles.org → Connection reset
❌ file.io → 返回的 JSON 其实是错误页面
❌ 0x0.st → 503 Service Unavailable
❌ transfer.sh → 连不上

最后靠三招稳住:

  1. ==强制走 HTTP/1.1==。OkHttpClient.Builder().protocols(listOf(Protocol.HTTP_1_1))——这一步把大量奇怪的协议错误直接干掉
  2. 每次上传建新 client,不复用连接池,避免「已死连接被复用」
  3. 每个服务设短超时,失败立刻跳到下一个,不卡整个转写流程
  4. catbox 请求体必须带 reqtype=fileupload——少了这个字段直接失败

这块前前后后改了大概 6 次代码才稳定下来,是整个项目最耗时间的一环。

问题 6:URL 空字符串的坑

有一次我把 URL 写成了空字符串 ""——在 Kotlin 里写了 val url = "" ?: fallbackUrl。结果 "" 不是 null,fallback 永远不触发。报错:Expected URL scheme 'http' or 'https' but no scheme was found for

改成 url.trim().ifEmpty { fallbackUrl } 才过。==这种「看起来是 null 检查,其实是空字符串检查」的坑,语言特性里最容易踩的一类==。

第三阶段:把文字变成能用起来的体验

到第二阶段收工,App 已经能跑通:录音 → 上传 → 转写 → 显示文字。

但用起来很别扭:

  • 「查看录音」按钮跳去文字列表,点一条就开始播放——不是我要的
  • 转写结果没法一键复制,还得手动长按选
  • 录音文件越堆越多,没有删除功能

后来加了四个东西:

1. 独立的「录音管理页」

跟手机自带录音 App 一样:一条条列出来,左边播放按钮、右边删除按钮,中间显示时长和文件大小。删除时同步干掉数据库记录 + 本地文件,Flow 自动刷新其他页面。

  1. 「每日灵感」卡片加一键复制

按日期分组展示,卡片右上角一个小复制按钮,点一下当天全部灵感进剪贴板。

  1. 「全部记录」回归纯粹的文字浏览

只留文字展示 + 单条复制。播放录音统一去「录音管理页」。

  1. API Key 配置页

首页右上角齿轮按钮,弹窗分「新版控制台」和「旧版控制台」两种格式输入,配置存在本地 SharedPreferences。

技术栈速览(给想抄作业的人)

选型
语言
!!Kotlin!!
数据库
!!Room!!(实体 + DAO + Database)
异步
协程(CoroutineScope + Flow 实现自动刷新)
网络
!!OkHttp!!
ASR
火山引擎豆包语音识别(大模型版)
文件中转
catbox.moe + 多个备用服务的 fallback 链

关键文件位置:

  • 悬浮球与录音:FloatingRecorderService.kt
  • 文件上传 fallback 链:FileUploader.kt
  • 豆包 ASR 调用:DoubaoAsrClient.kt
  • 数据库:InspirationRepository.kt
  • 录音管理页:AudioListActivity.kt

一点感受

这次搞下来,有两件事我特别想聊。

第一件:==AI 把「动手做」的门槛打下来了,但「做好」的门槛一点没降==。搭个能录音的 App 一小时,做到「录完就出正确文字」——十几个小时。

第二件:==真实项目里 80% 的 bug 出在接口和边界条件==。协议版本、服务通不通、编码格式、权限声明、字段取不取得到、空串还是 null——这些东西教程不会重点讲,但真干起来卡你最久。

如果你也有类似的「小想法」,我的建议就一句:

==别想太多,先做出来再说。==

哪怕第一版长得丑、跑得慢,至少你知道它能不能真正解决你的问题。后面再迭代,永远比一开始追求完美高效得多。

写在最后

如果你也有「脑子里一直想做但觉得不完美就没动手」的小工具,欢迎留言聊聊——说不定你的痛点就是下一个半天能跑通的 App 原型。

我在这里分享 AI 时代普通人动手造东西的真实过程,不包装、不贩卖焦虑。觉得有用的话,点个关注,下次更新不迷路。