1. 从零到上线:为什么我选择 WorkBuddy 这条路线
去年年底我接了个私活,帮一个做线下活动策划的朋友搭一套内部用的移动端工具。需求说起来不复杂:活动排期、物料清单、现场签到、简单数据看板,团队十来个人用。但真动手的时候问题就来了——预算有限,没有专职后端,前端就我一个人,还得同时出 Android 和 iOS 两个端。我前后试过三条路线:纯原生双端开发、跨端框架、以及后来定下来的 WorkBuddy 加 WebView 加 Supabase 这套组合。
前两条路线的坑我踩得够多了。纯原生双端意味着我要维护两套代码,改一个字段两边都得动,工期直接翻倍。跨端框架倒是省事,但打包体积和调试体验在低端安卓机上实在难受,热更新链路也经常出幺蛾子。最后我选了第三条路:用 WorkBuddy 做壳和工程编排,核心页面用 WebView 承载,数据层直接挂 Supabase。这套方案最大的好处是——我只需要写一套 Web 代码,壳层负责原生能力调用,数据层不用自己搭服务器。
这篇文章我想把这六个阶段完整拆开讲:从环境准备、壳工程搭建、WebView 集成、Supabase 接入、打包签名,一直到上线前的自测清单。中间我会把踩过的十六个坑一个个标出来,包括 Android 各版本 WebView 内核差异、content://协议的文件访问限制、Supabase 行级安全策略配置、以及打包时那个让人抓狂的could not determine the dependencies of task ':app:compileDebugJavaWithJavac'报错。适合谁看?如果你也是一个人扛一个 App、预算紧、想快速上线验证想法,这套路线应该能帮你省下不少时间。
先说清楚这套方案的边界。它不适合做重图形渲染的游戏,不适合对启动速度要求极致的产品,也不适合需要深度调用系统底层能力的场景。但如果你做的是工具类、内容类、内部管理类 App,页面以表单、列表、图表为主,那这套组合的性价比非常高。我实测下来,一个中等复杂度的工具 App,从零到能装到手机上跑,熟练的话三天内能搞定。
2. 六个阶段的整体拆解与方案选型逻辑
2.1 为什么是这六个阶段
我把整个流程拆成了六个阶段,这个拆法不是拍脑袋来的,而是按照"依赖关系"和"可验证节点"两个维度切的。每个阶段结束时都有一个明确的验收标准,这样你不会在某个环节卡住却不知道问题出在哪。
| 阶段 | 核心任务 | 验收标准 | 预估耗时 |
|---|---|---|---|
| 第一阶段 | 环境与工具链准备 | 能跑通 Hello World 壳工程 | 0.5 天 |
| 第二阶段 | WorkBuddy 壳工程搭建 | 真机安装成功,能加载本地页面 | 1 天 |
| 第三阶段 | WebView 集成与原生桥接 | H5 能调用原生能力,返回正常 | 1 天 |
| 第四阶段 | Supabase 数据层接入 | 增删改查跑通,权限策略生效 | 1 天 |
| 第五阶段 | 打包签名与渠道配置 | 出正式包,能装到非调试机 | 0.5 天 |
| 第六阶段 | 上线前自测与灰度 | 自测清单全过,灰度无崩溃 | 1 天 |
这个顺序有个关键逻辑:先让壳能跑,再让页面能显示,最后才接数据。很多人一上来就写业务逻辑,结果壳工程本身有问题,排查起来要同时怀疑三层,效率极低。我建议你严格按这个顺序走,每步都验证通过再往下。
2.2 技术选型背后的取舍
选 WorkBuddy 而不是纯手写 Android 工程,核心原因是它把工程编排、依赖管理、构建流程这些重复劳动封装掉了。你不需要从零配 Gradle、不需要手动处理各种 SDK 版本冲突,它给你一个能直接跑的骨架。这对独立开发者来说太重要了——我的时间应该花在业务上,不是花在跟构建工具搏斗上。
WebView 承载页面的选择,是因为我的页面全是表单和列表,用原生写反而慢。WebView 的渲染一致性在 Android 5.0 以上已经相当不错了,配合合理的缓存策略,体验和原生差距很小。这里有个前提:你得控制好 WebView 的版本,后面我会专门讲这个坑。
Supabase 替代自建后端,是因为它把数据库、鉴权、存储、实时订阅全打包了,而且有免费额度。对于十来个人的内部工具,免费额度完全够用。它的行级安全策略(RLS)是个好东西,能在数据库层就把权限卡死,不用在应用层写一堆判断逻辑。
提示:这套组合的核心思路是"把能外包的都外包,自己只写业务"。选型时不要追求技术先进性,要追求"我一个人能不能维护得动"。
2.3 十六个坑的分布规律
我把踩过的坑按阶段归了类,你会发现大部分坑集中在第三和第四阶段,也就是 WebView 集成和数据层接入。这两个阶段涉及跨层交互,出问题的概率天然就高。
- 环境阶段 2 个坑:SDK 版本冲突、Gradle 缓存污染
- 壳工程阶段 2 个坑:包名规范、图标适配
- WebView 阶段 5 个坑:内核版本、文件协议、历史版本兼容、进度条、返回键处理
- Supabase 阶段 4 个坑:RLS 策略、实时订阅断连、字段类型映射、免费额度限制
- 打包阶段 2 个坑:签名配置、依赖解析失败
- 上线阶段 1 个坑:灰度策略
这个分布告诉你一个事实:跨层交互是 bug 重灾区。所以每当你写一段涉及两层以上交互的代码,就要格外小心,多打日志,多做边界测试。
3. 环境准备与壳工程搭建的实操细节
3.1 工具链安装的版本选择
环境这块我踩的第一个坑就是版本。Android Studio 的版本、JDK 的版本、Gradle 的版本,这三者之间有严格的兼容矩阵。我一开始图省事装了最新的 Android Studio,结果它自带的 JDK 版本和我项目里配的 Gradle 版本对不上,构建直接报错。
我的建议是:不要追最新版。选一个发布半年以上、社区反馈稳定的版本组合。具体来说,Android Studio 选稳定版而非预览版,JDK 选 17(这是目前兼容性最好的 LTS 版本),Gradle 版本跟着 Android Studio 的推荐走,不要手动改。
安装完 Android Studio 后,第一件事是设置中文界面。很多人找不到入口,其实在 Settings 里的 Plugins 搜 Chinese 语言包,装完重启就行。这个不是必须的,但如果你英文一般,中文界面能省不少查文档的时间。
第二个坑是 SDK 路径。默认路径里有空格(比如C:\Program Files\...),某些构建脚本处理不了带空格的路径。我建议你把 SDK 装到一个纯英文无空格的路径下,比如D:\Android\Sdk。这个坑很隐蔽,报错信息也不会直接告诉你路径问题,只会说找不到某个组件。
3.2 壳工程初始化的关键配置
WorkBuddy 初始化工程的时候,有几个配置项必须一开始就定好,后面改起来很麻烦。
包名(applicationId):这个一旦上线就不能改了,改了就相当于换了个 App。命名规范用反域名,比如com.yourcompany.yourapp。不要用默认的com.example.xxx,很多应用市场会拒绝上架。
最低支持版本(minSdkVersion):这个决定了你的 App 能装到多老的手机上。设太低,很多新 API 用不了;设太高,用户覆盖少。我的经验是设到 Android 7.0(API 24),这个版本以上的设备覆盖率已经很高了,而且 WebView 的能力也够用。
目标版本(targetSdkVersion):这个要跟着应用市场的要求走,一般设成最新的稳定版。它影响的是系统的行为兼容性,比如权限申请方式、后台限制等。
初始化完成后,先别急着写业务,跑一遍空工程,确认能在真机上装起来。这一步能帮你排除掉大部分环境问题。
3.3 图标与启动页的适配
图标这块看着简单,其实坑不少。Android 从 8.0 开始引入了自适应图标(Adaptive Icon),需要提供前景层和背景层两张图。如果你只放一张传统图标,在新系统上会显示成一个带白边的方块,很难看。
我的做法是准备三套资源:传统图标(mipmap 各密度)、自适应图标的前景和背景、以及启动页的 logo。用 Android Studio 自带的 Image Asset 工具生成,能自动帮你切好各种尺寸。
启动页这块,Android 12 之后系统强制要求用 SplashScreen API,不能再自己写一个 Activity 当启动页了。如果你不处理,启动时会先显示一个系统默认的图标页,再跳你的页面,中间会有明显的闪烁。用官方 API 配置好背景色和图标,过渡就顺滑了。
注意:图标和启动页的适配问题,在调试阶段往往看不出来,因为调试机通常是新系统。等你装到老设备上才发现问题,返工成本很高。建议一开始就用多台不同版本的设备测试。
4. WebView 集成:五个坑的完整排查记录
4.1 WebView 内核版本差异与检测
这是我在整个项目里踩得最深的坑。Android 的 WebView 内核版本是跟着系统走的,不同版本的系统自带的内核差异巨大。Android 5.0 自带的是 Chromium 37,Android 8.0 是 58,Android 12 是 90 多。这意味着同一段 CSS 或 JS,在不同设备上表现可能完全不同。
我遇到的具体问题是:用了某个较新的 CSS 属性,在新设备上正常,在老设备上直接布局错乱。排查了半天才发现是内核版本太低不支持。
解决办法有两个。一是降级你的前端技术栈,不用太新的特性,或者用 polyfill 兜底。二是在应用内集成独立的内核,不依赖系统自带的。第二种方案体积会增加几十兆,但兼容性有保障。对于内部工具,我选了第一种,成本更低。
检测内核版本的方法很简单,在 WebView 里执行一段 JS 读取 userAgent,里面会带 Chrome 的版本号。你也可以用在线的 WebView 版本检测网站,输入 userAgent 就能看到对应的内核版本和支持的特性列表。我建议在应用启动时把版本号打到日志里,方便排查问题。
4.2 content:// 协议的文件访问限制
Android 7.0 之后,系统收紧了文件访问权限,不允许直接通过file://协议访问应用外部存储。你必须用content://协议,配合 FileProvider 来共享文件。
这个改动影响很大。比如你的 H5 页面里有个上传功能,用户选完图片后,拿到的路径是content://com.xxx.fileprovider/...这种格式,而不是传统的文件路径。如果你直接用这个路径去读文件,会失败。
正确的做法是通过 ContentResolver 打开输入流,把文件内容读出来再处理。或者用 FileProvider 的 API 把 content URI 转成临时文件路径。我当时的场景是 H5 要预览用户选的图片,最后是在原生层把图片读成 base64 传给 H5,虽然效率低点,但兼容性最好。
类似的还有从其他应用分享过来的文件,路径格式五花八门,有content://com.baidu.searchbox.fileprovider/...、content://com.tencent.wework.fileprovider/...等等。处理这类 URI 时,不要假设它的格式,统一用 ContentResolver 去解析。
4.3 WebView 历史版本兼容处理
有些场景下你需要回退到旧版本的 WebView 内核,比如某个新版本引入了 bug 导致页面崩溃。Android 允许你卸载 WebView 的更新,回退到系统自带版本。但这个过程对普通用户来说太复杂了,不能作为正式方案。
我的处理方式是做特性检测而非版本检测。不要写"如果内核版本大于 X 就用某功能",而是写"如果这个 API 存在就用,不存在就走降级逻辑"。这样无论用户的内核是什么版本,代码都能正确运行。
具体实现上,用if ('某API' in window)这种检测方式,比读版本号可靠得多。因为版本号和实际支持的特性之间并不是严格对应的,厂商还可能做定制。
4.4 进度条的合理实现
WebView 加载页面需要时间,没有进度提示用户会以为卡死了。但进度条也不能乱做,我见过很多实现是在原生层盖一个进度条,页面加载完再隐藏,结果页面白屏的时候进度条也在转,用户更困惑。
我的做法是双层进度提示。原生层放一个细的顶部进度条,跟着 WebView 的加载进度走,这个用onProgressChanged回调就能拿到。同时 H5 页面内部自己管理内容区的骨架屏,数据加载完再渲染真实内容。这样用户看到的是:顶部进度条在走,内容区有骨架占位,体验就自然了。
进度条的颜色和高度也有讲究。高度建议 2 到 3 像素,太粗了显得笨重。颜色用主题色,和整体风格统一。不要用那种彩虹色的动画进度条,看着花哨,实际很廉价。
4.5 返回键与页面栈的联动
Android 的物理返回键和 H5 的页面栈是两套独立的历史记录。如果不做联动,用户按返回键会直接退出 App,而不是回到上一个 H5 页面,体验很割裂。
处理逻辑是这样的:在原生层拦截返回键事件,先判断 WebView 能不能回退(canGoBack()),能回退就调goBack(),不能回退再执行退出逻辑。但这里有个细节:如果 H5 页面里有弹窗或者抽屉打开着,应该先关闭弹窗,而不是回退页面。
我的实现是在 H5 层暴露一个方法给原生调用,返回当前是否有可关闭的浮层。原生拦截返回键时先问 H5,有浮层就关浮层,没有再看能不能回退。这套逻辑写下来不复杂,但能显著提升体验。
实操心得:WebView 相关的 bug 有个特点——在模拟器上往往复现不了,必须用真机,而且最好是低端机。我专门留了一台几年前的千元机做兼容性测试,很多问题只有在那上面才暴露。
5. Supabase 数据层接入的四个关键坑
5.1 行级安全策略的正确配置
Supabase 的行级安全策略(RLS)是个双刃剑。配好了,权限管理非常省心;配错了,要么数据全暴露,要么谁都读不到。
我一开始犯的错是:建完表忘了开 RLS,结果表默认对所有登录用户开放。虽然当时只是测试数据,但想想还是后怕。建表后的第一件事就是开 RLS,这应该成为肌肉记忆。
开完 RLS 后,默认是拒绝所有访问,你需要显式地写策略。常见的策略模式有几种:按用户 ID 过滤(只能看自己的数据)、按角色过滤(管理员能看全部)、按时间过滤(只能看未过期的)。写策略的时候要注意,策略是"或"的关系,只要有一条策略通过就放行。
我踩的坑是策略写得太宽。比如写了个"登录用户可读",结果所有登录用户都能读到所有人的数据。正确的写法应该是auth.uid() = user_id,把当前用户 ID 和数据的归属字段做匹配。
5.2 实时订阅的断连重连
Supabase 的实时订阅(Realtime)基于 WebSocket,网络切换或者长时间空闲后会断连。如果不做重连处理,用户会发现数据不更新了,但界面没有任何提示。
我的处理是监听连接状态 + 自动重连 + 状态提示。Supabase 的客户端库提供了连接状态的回调,断连时在界面上显示一个"连接中"的小提示,重连成功后自动隐藏。重连逻辑用指数退避,第一次隔 1 秒重试,第二次 2 秒,第三次 4 秒,避免频繁重试把服务器打挂。
还有个细节:重连成功后要重新拉一次全量数据,因为断连期间的数据变更可能已经错过了。不能只依赖增量推送。
5.3 字段类型映射的陷阱
Supabase 底层是 PostgreSQL,字段类型比 JavaScript 丰富得多。从数据库读出来的数据,类型可能和你预期的不一样。
我遇到的典型问题:数据库里的timestamp字段,读出来是 ISO 格式的字符串,直接new Date()在某些老设备上解析会失败。还有numeric类型,读出来可能是字符串而不是数字,做运算前必须先转换。
我的做法是在数据层做一层统一的转换,把数据库返回的原始数据规范化成前端好用的格式。这层转换虽然多写点代码,但能避免业务层到处做类型判断。
5.4 免费额度的边界认知
Supabase 免费版有额度限制,主要是数据库大小、带宽、月活用户数。对于内部工具,这些额度通常够用,但你要清楚边界在哪。
我踩的坑是存储额度。Supabase 的存储(Storage)和数据库是分开算的,我一开始把用户上传的图片都存到 Storage,没注意额度,结果某天上传失败才发现超了。后来改成图片压缩后再上传,并且定期清理不再使用的文件。
还有个容易忽略的是并发连接数。免费版的数据库连接数有限,如果你的应用同时在线人数多,可能会遇到连接被拒的情况。解决办法是用连接池,或者把一些查询做缓存。
提示:用免费服务一定要养成看用量面板的习惯。我每周会看一眼各项指标的使用情况,快接近上限时提前做优化,而不是等出问题了才手忙脚乱。
6. 打包签名与上线前的完整自测
6.1 签名配置与依赖解析失败排查
打包这块我踩的坑是那个经典的报错:could not determine the dependencies of task ':app:compileDebugJavaWithJavac'。这个报错信息很误导,它说的是"无法确定任务依赖",但真正的原因往往是某个依赖库下载失败或者版本冲突。
排查思路是这样的:先看完整的错误堆栈,找到could not resolve all task dependencies后面跟的具体是哪个库。然后检查这个库的版本号是否存在于仓库中,网络是否能访问到仓库。我遇到的情况是某个库的版本号写错了,仓库里根本没有这个版本,但报错信息没直接说。
签名配置相对简单,但有几个点要注意。密钥库文件(keystore)一定要备份,丢了就再也无法更新应用了。密码和别名也要记牢,建议存在密码管理器里。签名配置写在build.gradle里时,不要把密码明文提交到代码仓库,用环境变量或者单独的配置文件,并且把配置文件加入.gitignore。
6.2 上线前的自测清单
上线前的自测我整理了一份清单,每次发版都过一遍。这份清单帮我拦下了不少低级问题。
| 检查项 | 检查方法 | 常见问题 |
|---|---|---|
| 冷启动速度 | 杀进程后重新打开,计时 | 超过 3 秒需优化 |
| 弱网表现 | 限速到 2G 网络测试 | 白屏无提示 |
| 断网恢复 | 断网操作后恢复网络 | 数据不刷新 |
| 返回键逻辑 | 各页面反复按返回 | 直接退出应用 |
| 权限拒绝 | 拒绝所有权限后使用 | 崩溃 |
| 后台切换 | 切后台再切回 | 状态丢失 |
| 低电量模式 | 开启省电模式使用 | 功能异常 |
| 大字体模式 | 系统字体调到最大 | 布局错乱 |
这份清单里的每一项我都实际遇到过问题。比如大字体模式,系统字体调大后,固定高度的按钮文字会溢出,必须用自适应高度。还有低电量模式,某些后台任务会被系统限制,导致数据同步失败。
6.3 灰度发布与崩溃监控
正式全量发布前,一定要做灰度。先发给一小部分用户,观察一两天,没问题再逐步扩大范围。灰度期间重点看两个指标:崩溃率和核心功能的成功率。
崩溃监控我建议接入一个第三方的崩溃收集服务,能自动上报崩溃堆栈。没有这个的话,用户遇到崩溃只会默默卸载,你根本不知道。接入成本很低,但价值极高。
灰度期间还要留意用户反馈。内部工具的好处是用户就在你身边,有问题能直接问。我一般会主动找几个用得比较多的同事,问问有没有遇到什么奇怪的现象。很多 bug 是用户觉得"可能是我操作不对"就没反馈,你主动问才能挖出来。
7. 一些没写进文档的经验
这套方案我后来又用在了两个项目上,越用越顺手。有几个经验是文档里不会写的,但实际很重要。
关于 WorkBuddy 和 CodeBuddy 的选择:这两个工具定位不同。WorkBuddy 偏向工程编排和构建流程,CodeBuddy 偏向代码生成和辅助。我的用法是 WorkBuddy 管工程骨架,CodeBuddy 帮我写一些重复性的业务代码。两者配合,效率提升明显。但不要指望它们能帮你做架构决策,那还是得自己来。
关于 WebView 的调试:Chrome 的远程调试功能是救命的。手机连上电脑,在 Chrome 里输入chrome://inspect,就能像调试网页一样调试 WebView 里的页面。这个功能一定要会用,能省下大量打日志的时间。
关于 Supabase 的本地开发:Supabase 提供了本地开发环境,可以在本地跑一套完整的服务。我建议开发阶段用本地环境,避免污染线上数据。等测试通过再同步到线上。本地环境的启动需要 Docker,配置稍微麻烦点,但值得。
关于版本管理:这套方案涉及壳工程、H5 页面、数据库 schema 三部分,版本要统一管理。我的做法是用一个版本号贯穿三者,发版时三部分一起打 tag。这样出问题能快速定位到是哪个版本引入的。
关于文档:不要省文档。我一个人开发的时候也觉得写文档浪费时间,但隔一个月回头看代码,没有文档真的想不起来当时为什么这么写。我的做法是每个关键决策点写一段注释,说明"为什么这么做"而不是"做了什么"。前者比后者有价值得多。
最后分享一个我踩过的坑:不要在生产环境直接改数据库 schema。我有次图省事,直接在线上加了个字段,结果忘了同步更新 RLS 策略,导致新字段对所有用户可读。虽然字段本身不敏感,但这个教训让我养成了所有 schema 变更都走迁移脚本的习惯。迁移脚本能保证本地和线上一致,也能追溯每次变更。
这套方案不是银弹,它有自己的适用边界。但如果你和我一样,是一个人扛一个 App 的独立开发者,预算有限、时间有限,那它值得一试。关键是把每个阶段的验收标准卡死,不要带着问题往下走,否则问题会像滚雪球一样越滚越大。