1. 为什么我选择用 WorkBuddy 从零搭一个能上线的 App
去年年底我接了个私活,帮一个做线下活动策划的团队做一套内部工具。需求很明确:活动执行人员要在现场用手机签到、拍照上传、查看任务清单,管理端要能实时看到进度。预算不高,工期给了六周,团队里加上我一共两个人。这种项目如果用原生 Android 加 iOS 双端开发,光环境搭建和两端适配就能把时间吃干净,所以我从一开始就排除了纯原生方案。
我最终选定的路线是:WorkBuddy 作为 AI 辅助开发的主工作台,产出以 WebView 为壳的混合 App,后端数据层用 Supabase 兜底,最终打包成可上架的 Android 应用。这套组合不是拍脑袋定的,而是踩过几轮坑之后收敛出来的结果。WorkBuddy 在这个流程里承担的角色,更像是一个“懂代码的搭档”——你把需求拆清楚给它,它能帮你把页面结构、接口调用、状态管理这些重复性劳动快速铺出来,而不是让你从MainActivity一行行手写。
这篇文章适合三类人看:一是手上有小体量 App 需求、想快速验证的独立开发者;二是习惯了写 Web 但想切入移动端的前端同学;三是已经在用 WorkBuddy 但总觉得“生成的东西跑不起来”的同行。我会把整个项目拆成六个阶段,把每个阶段里真实踩到的十六个坑全部摊开讲,包括 Android WebView 的版本兼容、Supabase 的行级安全策略、文件上传的 content URI 处理、以及打包上线前那些文档里不会写的细节。
先给一个整体判断:这套方案能上线,但前提是你得清楚哪些环节必须自己兜底。WorkBuddy 帮你提速,Supabase 帮你省后端,WebView 帮你省跨端成本,但三者之间的接缝处,才是真正决定项目能不能交付的地方。
2. 整体方案设计与技术选型拆解
2.1 为什么是 WorkBuddy 而不是纯手写
我一开始也试过纯手写。第一个版本用 Android Studio 建了个空项目,自己写 Activity、自己搭 RecyclerView、自己处理网络请求。写到第三天我就发现一个问题:这个项目的核心价值在于业务逻辑和现场体验,而不是“我会不会写 Android 原生”。签到、拍照、列表刷新这些功能,本质上都是模板化的东西,手写一遍除了消耗时间,并不会让产品更好。
WorkBuddy 的价值在这里就体现出来了。我把需求描述成自然语言——比如“一个带底部导航的页面,三个 Tab 分别是任务列表、签到、我的,任务列表每项显示标题、状态标签和截止时间”——它能直接给我可运行的页面骨架。我需要做的不是从零写,而是审查它生成的结构、调整业务逻辑、补上它没考虑到的边界情况。这个工作模式的效率差异,大概是从“砌砖”变成了“验收加装修”。
但这里有个前提:你得能看懂它生成的东西。如果你完全不懂 Android 项目结构、不懂 WebView 的基本配置,WorkBuddy 生成完你也不知道下一步该干什么。所以我的建议是,至少花半天时间把 Android 项目的基本目录结构、AndroidManifest.xml的作用、Gradle 的基本配置搞清楚,再开始用 WorkBuddy 提效。
2.2 WebView 壳方案的取舍逻辑
混合 App 这个路线争议一直很大。纯 Web 派觉得 WebView 性能差、体验割裂;纯原生派觉得混合方案是妥协产物。我的实际体验是:对于内部工具类、内容展示类、表单交互类的 App,WebView 壳方案的性价比远高于纯原生。
这个项目的页面构成很简单:列表页、详情页、表单页、图片展示页。没有复杂动画,没有高频手势交互,没有重度图形渲染。这种场景下,WebView 加载本地 HTML 资源的首屏时间可以控制在 1 秒以内,页面切换的流畅度在千元机上也能接受。真正需要原生的只有两件事:调用系统相机/相册和文件上传。这两块我用原生桥接的方式处理,其余全部交给 WebView。
选 WebView 还有一个隐性好处:迭代成本极低。活动策划这个行业需求变得快,今天要加个字段,明天要改个流程。如果页面是 HTML 写的,我改完直接替换assets目录里的文件重新打包就行,不需要动 Java/Kotlin 层。如果后面要做热更新,WebView 方案的改造空间也比原生大得多。
2.3 Supabase 在后端扮演的角色
Supabase 在这个项目里承担了数据库、文件存储和身份认证三件事。选它而不是自己搭后端,核心原因是我不想在六周的项目里花两周去写 CRUD 接口和部署服务器。
Supabase 提供的是 Postgres 数据库加自动生成的 REST API,建完表之后前端直接调 SDK 就能读写。文件存储这块,签到照片直接传到 Storage 桶里,返回一个 URL 存到数据库字段就行。身份认证用它的 Auth 模块,手机号加验证码登录,省掉了自己写登录态管理的麻烦。
但 Supabase 有个必须提前想清楚的点:行级安全策略(RLS)。默认情况下,如果你不开启 RLS,任何拿到 anon key 的人都能读写你的全表数据。这个坑我在测试阶段踩过一次——当时忘了给任务表开 RLS,结果用浏览器直接请求接口就能拉到所有数据。后面我会详细讲怎么配。
2.4 三者的衔接关系
把这三个东西串起来看:WorkBuddy 负责生成前端页面和原生壳代码,WebView 负责承载前端页面并桥接原生能力,Supabase 负责数据持久化和文件存储。它们之间的数据流是这样的:
用户在 WebView 里操作页面,页面通过 Supabase JS SDK 直接读写数据库;需要拍照时,页面通过 JavaScript Bridge 调用原生相机,原生拍完把图片路径回传给页面,页面再把图片上传到 Supabase Storage,最后把返回的 URL 写入数据库。
这个链路里最容易出问题的环节是原生与 WebView 的通信,以及文件 URI 的转换。Android 从 7.0 开始收紧了文件权限,从相机拿到的content://URI 不能直接给 WebView 用,必须经过FileProvider转换。这个细节后面会展开。
3. 六个阶段的完整实操记录
3.1 第一阶段:需求拆解与 WorkBuddy 提示词设计
很多人用 WorkBuddy 效果不好,问题出在第一步:提示词太笼统。你如果说“帮我做一个活动管理 App”,它生成的东西大概率是四不像。我的做法是把需求拆到“页面级”和“字段级”。
具体来说,我会先写一份需求清单,格式是这样的:
- 页面一:任务列表。字段包括任务标题、负责人、状态(待执行/进行中/已完成)、截止时间。支持按状态筛选。
- 页面二:任务详情。显示任务全部字段,底部有“签到”按钮和“上传照片”按钮。
- 页面三:签到记录。按时间倒序显示当前用户的所有签到记录,每条显示时间、地点、照片缩略图。
- 页面四:我的。显示用户头像、昵称、手机号,底部有退出登录按钮。
然后我把这份清单直接喂给 WorkBuddy,让它先生成页面结构和数据模型。这里有个技巧:让它先输出数据表设计,确认无误后再生成页面代码。因为页面可以改,数据表结构一旦定了再改成本很高。
提示:WorkBuddy 生成的数据表设计一定要自己过一遍。它有时候会把该拆表的字段塞在一起,比如把“签到地点”和“任务地点”混在一个字段里。这种问题越早发现越好。
3.2 第二阶段:Android 壳工程搭建与 WebView 配置
WorkBuddy 可以生成 Android 项目的基础结构,但我建议这一步自己用 Android Studio 建一个空项目,然后把 WorkBuddy 生成的代码往里填。原因是 Android Studio 会自动处理 Gradle 版本、SDK 版本、依赖下载这些琐事,比让 WorkBuddy 从零生成一个完整工程要稳。
建好项目后,核心工作是配置 WebView。这里有几个关键参数:
WebView webView = findViewById(R.id.webview); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setAllowFileAccess(true); settings.setAllowContentAccess(true); settings.setMediaPlaybackRequiresUserGesture(false); webView.addJavascriptInterface(new NativeBridge(this), "AndroidBridge"); webView.loadUrl("file:///android_asset/www/index.html");setJavaScriptEnabled是必须的,否则页面脚本跑不起来。setDomStorageEnabled要开,因为 Supabase 的 JS SDK 会用到 localStorage 存会话信息。setAllowFileAccess和setAllowContentAccess是为了让 WebView 能读取本地资源和处理文件 URI。
addJavascriptInterface是原生桥接的入口。我在NativeBridge类里暴露了三个方法:takePhoto()、pickImage()、getAppVersion()。页面里通过window.AndroidBridge.takePhoto()就能调用原生相机。
注意:
addJavascriptInterface在 Android 4.2 以下有安全漏洞,但这个项目最低支持版本设的是 Android 7.0,所以不用考虑。如果你的项目要兼容更老的版本,需要做版本判断。
3.3 第三阶段:Supabase 数据层搭建与 RLS 配置
Supabase 这边我做了三张表:tasks、checkins、profiles。建表直接在 Supabase 控制台的 SQL Editor 里写:
create table tasks ( id uuid default gen_random_uuid() primary key, title text not null, assignee uuid references auth.users(id), status text default 'pending', deadline timestamptz, created_at timestamptz default now() ); create table checkins ( id uuid default gen_random_uuid() primary key, user_id uuid references auth.users(id), task_id uuid references tasks(id), photo_url text, location text, created_at timestamptz default now() );建完表之后,立刻开启 RLS。这一步不能省。开启方式是在控制台点表名,找到 Row Level Security 开关,打开它。然后添加策略:
-- 用户只能看自己的任务 create policy "Users can view own tasks" on tasks for select using (auth.uid() = assignee); -- 用户只能插入自己的签到记录 create policy "Users can insert own checkins" on checkins for insert with check (auth.uid() = user_id);RLS 的逻辑是:没有匹配的策略,操作就被拒绝。所以如果你只加了 select 策略没加 insert 策略,插入就会失败。这个设计一开始让我困惑了很久,后来想明白了——它默认是“全部禁止”,你显式允许什么才能做什么。
3.4 第四阶段:原生桥接与文件上传链路打通
这是整个项目最折腾的部分。WebView 里的页面要拍照,流程是这样的:
- 页面调用
window.AndroidBridge.takePhoto() - 原生启动相机 Intent,拍照后拿到
content://URI - 原生把 URI 转成 WebView 能访问的路径
- 原生通过
evaluateJavascript回调把路径传给页面 - 页面用这个路径读取文件,上传到 Supabase Storage
第三步的转换是关键。Android 7.0 之后,直接暴露file://URI 会触发FileUriExposedException。正确做法是用FileProvider:
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.app.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>file_paths.xml里定义可共享的目录:
<paths> <external-files-path name="images" path="Pictures/" /> <cache-path name="cache" path="/" /> </paths>拍照后,原生把图片存到getExternalFilesDir(Environment.DIRECTORY_PICTURES)目录下,然后用FileProvider.getUriForFile()生成一个 content URI。这个 URI 通过evaluateJavascript传给页面时,页面拿到的是字符串形式的 URI,需要用fetch读取成 Blob 再上传。
实操心得:WebView 里用
fetch读取content://URI 在部分 Android 版本上会失败。我的做法是原生直接把图片转成 Base64 字符串传给页面,页面再转成 Blob 上传。虽然多了一次编码解码,但兼容性最好。图片压缩到 800px 宽、质量 70%,Base64 字符串大概 100KB 左右,传输没有压力。
3.5 第五阶段:页面联调与状态同步
页面和 Supabase 联调阶段,我遇到的最大问题是登录态同步。Supabase 的 JS SDK 会把会话存在 localStorage 里,但 WebView 的 localStorage 在 App 重启后可能被清除。我的解决方案是在原生层用 SharedPreferences 存一份 refresh token,App 启动时通过桥接把 token 注入到页面里,页面用supabase.auth.setSession()恢复登录态。
另一个问题是列表页的数据刷新。用户签到后返回列表页,列表页的状态没有更新。我一开始想用 WebView 的onPageFinished回调来触发刷新,但这样会导致每次页面加载都刷新一次,体验不好。后来改成用事件机制:签到成功后,页面通过桥接通知原生,原生再通过evaluateJavascript调用列表页的刷新函数。这样只有真正需要刷新时才刷新。
3.6 第六阶段:打包上线前的检查清单
打包前我列了一份检查清单,逐项过:
AndroidManifest.xml里的权限是否齐全:相机、存储、网络build.gradle里的minSdkVersion和targetSdkVersion是否合理- 签名配置是否完成,keystore 文件是否备份
- WebView 加载的本地资源是否全部打包进
assets目录 - Supabase 的 anon key 是否写在了前端代码里(这是正常的,但要确保 RLS 配好)
- 应用图标和启动页是否替换
- 隐私政策页面是否准备好(上架应用商店需要)
这份清单里,签名配置和隐私政策是最容易漏的。签名文件如果丢了,后续就无法更新应用。隐私政策页面如果没准备,国内应用商店基本过不了审。
4. 十六个坑的完整复盘与排查技巧
4.1 WebView 版本兼容相关的四个坑
坑一:WebView 版本过低导致 ES6 语法报错。这个项目的最低支持版本是 Android 7.0,但部分老设备的 WebView 版本停留在 Chrome 51 左右,不支持async/await和可选链操作符。我的解决方案是在构建流程里加 Babel 转译,把 ES6+ 语法降级到 ES5。WorkBuddy 生成的代码默认是现代语法,这一步必须自己补。
坑二:localStorage在隐私模式下不可用。部分 Android 设备的 WebView 在隐私模式下会禁用 localStorage,导致 Supabase SDK 初始化失败。我的处理方式是加一层降级:检测 localStorage 是否可用,不可用则改用内存存储,同时提示用户关闭隐私模式。
坑三:file://协议下跨域请求被拦截。WebView 加载本地 HTML 时,页面里的fetch请求 Supabase 接口会被 CORS 拦截。解决方案是在 WebView 设置里开启setAllowUniversalAccessFromFileURLs(true),但这会带来安全风险。更稳妥的做法是把页面部署到一个本地 HTTP 服务上,或者用 Supabase 的 REST API 直接请求。
坑四:WebView 缓存导致页面更新不生效。替换assets目录里的 HTML 后,WebView 可能仍然加载旧版本。解决方案是在加载 URL 时加时间戳参数,或者在onPageFinished里调用webView.clearCache(true)。
4.2 Supabase 配置相关的四个坑
坑五:RLS 策略写错导致查询返回空数组。这个坑我踩了两次。第一次是策略里的auth.uid()写成了auth.user_id(),第二次是忘了给关联表加策略。排查方法是先在 SQL Editor 里用select * from tasks看有没有数据,如果有数据但接口返回空,那基本就是 RLS 的问题。
坑六:Storage 桶的权限没配。Supabase Storage 默认是私有的,上传的文件只有上传者能访问。如果页面需要显示其他用户上传的照片,需要把桶设为公开,或者生成带签名的临时 URL。我选的是公开桶加随机文件名,简单直接。
坑七:免费额度的限制。Supabase 免费版有 500MB 数据库和 1GB 存储的限制。签到照片如果不压缩,很快就能把存储撑满。我的做法是上传前在客户端压缩到 800px 宽、70% 质量,单张照片控制在 100KB 以内。
坑八:实时订阅在 WebView 里断连。Supabase 的 Realtime 功能在 WebView 切到后台后可能会断开。解决方案是监听visibilitychange事件,页面回到前台时重新订阅。
4.3 原生桥接相关的四个坑
坑九:evaluateJavascript必须在主线程调用。如果在子线程里调用,会直接崩溃。我的做法是用runOnUiThread包一层。
坑十:Base64 字符串过大导致传输失败。图片如果不压缩直接转 Base64,字符串可能超过 1MB,evaluateJavascript传输时会截断。压缩到 800px 后问题解决。
坑十一:相机权限被拒绝后没有回调。用户拒绝相机权限后,页面一直在等待回调。解决方案是在onRequestPermissionsResult里处理拒绝情况,通过桥接通知页面。
坑十二:FileProvider的 authority 冲突。如果项目里引入了其他库也用了FileProvider,authority 会冲突。解决方案是用自己应用的包名作为 authority 前缀。
4.4 打包上线相关的四个坑
坑十三:targetSdkVersion升级后权限行为变化。Android 11 之后,存储权限分成了READ_MEDIA_IMAGES等细分权限。如果targetSdkVersion升到 30 以上,需要适配新的权限模型。
坑十四:应用签名丢失。这个不用多说,keystore 文件一定要备份,最好存三份。
坑十五:隐私政策页面缺失。国内应用商店上架必须提供隐私政策页面,且要在应用内可访问。我是在“我的”页面底部加了一个链接,指向一个静态 HTML 页面。
坑十六:应用名称和图标不符合规范。部分商店对应用名称有长度限制,对图标有尺寸和格式要求。提前查清楚目标商店的规范,避免提交后被驳回。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 页面白屏 | WebView 未开启 JS | 检查setJavaScriptEnabled | 开启 JS 并重新加载 |
| 接口返回空数组 | RLS 策略未配置 | 在 SQL Editor 直接查询 | 添加对应的 select 策略 |
| 拍照后页面无响应 | 桥接回调未触发 | 检查evaluateJavascript调用 | 确保在主线程调用 |
| 图片上传失败 | Base64 过大 | 检查图片压缩参数 | 压缩到 800px 以下 |
| 登录态丢失 | localStorage 被清除 | 检查 WebView 缓存设置 | 用原生存储备份 token |
| 应用安装失败 | 签名冲突 | 检查 keystore 配置 | 卸载旧版本后重装 |
5. 这套方案后续还能怎么扩展
项目交付之后我复盘了一下,这套“WorkBuddy 加 WebView 加 Supabase”的组合,其实可以复用到很多类似场景。比如内部审批工具、活动报名系统、简单的电商展示页,核心逻辑都是“表单加列表加文件上传”,换汤不换药。
如果后续要继续优化,我会从三个方向入手。第一是引入热更新机制,把 WebView 加载的 HTML 资源放到远程服务器上,改完直接推送到客户端,省去重新打包的麻烦。第二是把原生桥接层抽象成通用模块,把相机、相册、文件选择、扫码这些常用能力封装好,下一个项目直接复用。第三是给 Supabase 加一层缓存,列表页的数据在本地存一份,弱网环境下也能看到内容。
最后分享一个我在这个项目里养成的小习惯:每次 WorkBuddy 生成代码后,先跑一遍再改。不要一上来就大改特改,先确认它生成的东西能跑起来,再在能跑的基础上做增量修改。这样出了问题你知道是它生成的问题还是你改出来的问题,排查成本低很多。我前两周就是因为跳过了这一步,在一个跑不起来的版本上改了三天,最后发现是它生成的 Gradle 配置里少了一个依赖。