
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 性能差、体验割裂纯原生派觉得混合方案是妥协产物。我的实际体验是对于内部工具类、内容展示类、表单交互类的 AppWebView 壳方案的性价比远高于纯原生。这个项目的页面构成很简单列表页、详情页、表单页、图片展示页。没有复杂动画没有高频手势交互没有重度图形渲染。这种场景下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。正确做法是用FileProviderprovider android:nameandroidx.core.content.FileProvider android:authoritiescom.example.app.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerfile_paths.xml里定义可共享的目录paths external-files-path nameimages pathPictures/ / cache-path namecache 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 tokenApp 启动时通过桥接把 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字符串可能超过 1MBevaluateJavascript传输时会截断。压缩到 800px 后问题解决。坑十一相机权限被拒绝后没有回调。用户拒绝相机权限后页面一直在等待回调。解决方案是在onRequestPermissionsResult里处理拒绝情况通过桥接通知页面。坑十二FileProvider的 authority 冲突。如果项目里引入了其他库也用了FileProviderauthority 会冲突。解决方案是用自己应用的包名作为 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 配置里少了一个依赖。