
1. 适配背景与核心思路这阵子我在做一款 Flutter 应用向鸿蒙端迁移的方案验证后端数据层正好用的是 Supabase。项目里十几个表、几十个 RPC 函数如果全手写模型类、手写查询构造器维护成本高得离谱。所以我第一反应是把 supabase_codegen 拉起来用数据库 schema 直接生成强类型 Dart 代码。本来以为只是加个 dev dependency 跑一下命令的事结果在鸿蒙工程里折腾了大半天。今天就把适配过程、踩坑点、以及最后跑通的方案完整记录下来。先捋一下目标Supabase 本身是开源的 BaaS 方案底层数据库是 PostgreSQL提供 REST API、Realtime、Auth、Storage 这些能力。Flutter 生态里对应的客户端是 supabase_flutter / supabase 这两个 Dart 包。supabase_codegen 的定位则是“代码生成器”它读取数据库的 schema 信息自动生成强类型的表模型、查询构造器和 RPC 调用封装。简单说你手里本来只有一张 users 表跑完 codegen 之后Dart 代码里就能直接写出supabase.from(UsersTable()).select().eq(User_.name, 张三)这种带补全、带类型检查的代码字段拼错会在编译期直接报错。鸿蒙化适配的核心难点不在 codegen 本身而在“链路”codegen 需要连接数据库拿到元数据需要在本机跑 Dart 命令生成后的代码又要在鸿蒙 Flutter 引擎里跑起来。大部分问题出在两端一是工具链依赖的包不一定兼容 OpenHarmony 的 Flutter 插件桥接层二是接口权限、网络权限、Linux 路径约束这些容易被忽略。所以这篇文章会拆成“原理层 - 环境层 - 实操层 - 排错层”四部分适合正在做鸿蒙应用、或者准备从 Android/iOS 迁移到鸿蒙的 Flutter 开发者参考。2. supabase_codegen 的工作原理与“强类型自动化映射”到底好在哪2.1 三条核心链路supabase_codegen 之所以能“自动映射”不是靠黑魔法而是靠三条链路协作第一条是数据库元数据读取。它会连接你的 Supabase 项目读取 information_schema / pg_catalog 里的表结构、字段类型、默认值、外键关系、枚举类型、视图、函数签名。这相当于把 PostgreSQL 的“自我介绍”转成一份中间结构。第二条是 Dart 代码模板生成。拿到中间结构之后codegen 把每一张表映射成一个 model 类把字段类型映射成 Dart 类型比如int8变成inttimestamptz变成DateTimejsonb变成dynamic或MapString, dynamic。同时生成对应的表查询类、数据变更方法、RPC 封装。这里的关键是有很多特殊的可空性、默认值、自增主键逻辑Codegen 用它自己的规则去处理比人手写稳定得多。第三条是运行时绑定。生成代码里包含了SupabaseClient的原生查询方法而不是自己拼 URL。比如final result await supabase .from(UsersTable()) .select() .eq(User_.status, active);UsersTable()和User_.status都是生成出来的常量。这样你写查询的时候IDE 能自动提示表名和字段名数据库里哪个字段改了重新生成一下代码工程里所有依赖它的地方会一起编译报错强迫你把相关逻辑改干净。2.2 强类型映射的价值很多人觉得“不就是多生成几个类吗有什么了不起”。实际用起来差异非常大。手写模型时代我经常犯这类错误把created_at记成create_at或者把is_admin当布尔值传结果后端存的是int2。这类问题在运行时才暴露往往是线上数据都写坏了才发现。强类型映射把这类错误直接前移到编译期。再一个是 RPC 函数。Supabase 支持你写 PostgreSQL 函数比如get_user_orders(user_id uuid)。手写调用时候要传参数名、参数类型拼错一个就 400。supabase_codegen 会把函数签名也算出来生成一个rpcGetUserOrders之类的函数参数类型完全由数据库签名决定对接成本低很多。还有一个被低估的点字段重命名。数据库里的last_login_at想改成last_seen_at如果手写模型你得全局搜替换走了 codegen只需要把数据库 schema 改了重新生成所有引用旧字段的代码会直接编译不过。这种方式倒逼你维护“数据库 - 代码”的一致性长期看非常舒服。2.3 鸿蒙化适配的技术坐标系鸿蒙的 Flutter 支持走的是 OpenHarmony 的 Flutter 引擎路线Dart runtime 本身是跨平台的所以纯 Dart 包一般能直接跑。问题集中在“平台通道”和“原生插件”。supabase_codegen 是纯 Dart 命令行工具按理说跟鸿蒙没有直接关系但生成代码后如果运行时用的是 supabase_flutter 全家桶它内部会依赖shared_preferences、url_launcher、app_links这些插件。这些插件在鸿蒙上如果没有对应实现轻则flutter pub get时报错重则运行时找不到平台通道。适配策略通常分两种一是让 supabase_flutter 链路里的每个插件都在鸿蒙侧找到替代实现二是绕开全家桶用核心supabase包 自定义的本地存储实现。我实际测试下来第二种更省心尤其是你只想用数据库和 RPC不用 Supabase 的 Auth 邮箱验证等功能时。3. 动手之前环境准备与版本选型3.1 工具链要求先说环境。我用的组合是Flutter SDK3.16 版本以上带 Dart 3OpenHarmony SDK / HarmonyOS NEXT 开发者预览版Supabase 服务端本地 Docker 版或云端项目都可以Dart 包管理器pubflutter --version dart --version建议在 pubspec.yaml 里把 supabase_codegen 放在 dev_dependenciesdependencies: supabase: ^2.0.0 dev_dependencies: supabase_codegen: ^1.0.0先别急着把 supabase_flutter 加进来等确定鸿蒙插件链路没问题再补。我个人教训是一开始想省事直接上全家桶结果 pub 依赖解析阶段就卡住了好几个插件在 OpenHarmony 平台没有发布包只能手动 fork。3.2 数据库 schema 准备codegen 的前提是数据库结构足够规范。建议先做一次 schema 整理把字段命名统一为snake_case主键统一为uuid或bigint时间字段带默认值。因为 codegen 的映射规则对命名和类型很敏感字段名乱七八糟会导致生成的 Dart 标识符非法。我一般先在本地用 Supabase CLI 起一个开发实例supabase init supabase start然后写supabase/migrations/下的 SQL 脚本建表。数据库起来之后可以用supabase db diff导出 schema SQL后面 offline 场景用得上。3.3 鸿蒙工程结构鸿蒙上的 Flutter 工程除了android/、ios/、web/这些传统平台目录还会有ohos/目录。这个目录里是鸿蒙原生的工程结构和配置文件比如module.json5、entry/src/main/ets/。Flutter 引擎在鸿蒙上通过一个项目模板集成Flutter 插件则需要有对应的鸿蒙平台的 TS/ETS 实现。所以你在引入任何 Flutter 插件之前最好先确认它在 ohos 端有没有被桥接。排查方法很简单看.flutter-plugins-dependencies文件里有没有ohos平台的 plugin 映射。如果有说明生态兼容如果里面没有但你又确实需要这个插件就得自己走“鸿蒙平台通道”适配的路子。4. 核心适配步骤从 schema 到生成 Dart 代码4.1 初始化配置supabase_codegen 的配置通常放在supabase.yaml里我的配置长这样supabase_url: http://127.0.0.1:54321 service_role_key: your-service-role-key output_dir: lib/database generate_rpc: true generate_views: truesupabase_url指向本地或云端的 Supabase API 地址service_role_key用来获取元数据。这里有一个安全层面要特别注意service_role_key权限极高千万别提交到公开仓库。建议把supabase.yaml加进.gitignore或者用环境变量占位。然后执行dart run supabase_codegen:generate首次运行会看到类似这样的输出Fetching database schema... Fetching RPC functions... Writing lib/database/database.dart Writing lib/database/tables/users.dart Writing lib/database/rpc/get_user_orders.dart4.2 生成结果解读生成的文件通常分成几类文件内容database.dart全局的 SupabaseClient 扩展入口聚合所有表与 RPCtables/*.dart每张表对应的模型类、表常量、查询封装rpc/*.dart每个数据库函数对应的强类型调用封装types/*.dart枚举、复合类型、自定义类型的 Dart 映射比如针对一张 users 表class User { final String id; final String name; final String? email; final DateTime createdAt; const User({ required this.id, required this.name, this.email, required this.createdAt, }); factory User.fromJson(MapString, dynamic json) { return User( id: json[id] as String, name: json[name] as String, email: json[email] as String?, createdAt: DateTime.parse(json[created_at] as String), ); } MapString, dynamic toJson() { id: id, name: name, email: email, created_at: createdAt.toIso8601String(), }; }这个类就是强类型映射的最终产物。后面在业务代码里我做数据展示时几乎不再接触原始MapString, dynamic,所有字段都有 IDE 提示。4.3 绕过全家桶插件链路到了鸿蒙这一层最关键的一步是确认工程依赖满足要求。我把依赖收敛成dependencies: supabase: ^2.0.0 web_socket_channel: ^2.4.0 http: ^1.1.0 uuid: ^3.0.7然后把所有涉及原生插件的代码隔离到一个单独的storage服务里。因为supabase包需要一个地方保存登录态 token我默认不依赖 supabase_flutter 的SharedPreferences而是用纯 Dart 的文件读写 path_provider的鸿蒙适配版。如果确实想保留下原生插件链路思路是找到每个插件的 ohos 实现比如用兼容性的shared_preferencesfork但这就要求你维护额外的 overridesdependency_overrides: shared_preferences: git: url: https://github.com/example/shared_preferences_ohos.git ref: main这种方案能跑但每次升级 Flutter 版本都要重新验证兼容性。我建议能不用就不用数据存储这层自己包一层接口后面无论是换localstorage还是换 SQLite 都很顺。4.4 接入鸿蒙工程的最后一步生成代码本身不直接触碰鸿蒙 API它只是普通 Dart 代码。真正要处理的是网络权限和证书。如果你在真机调试使用http://内网地址去连本地 Supabase需要在鸿蒙工程配置里加网络权限。找到ohos/entry/src/main/module.json5确认有{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }如果连的是线上 HTTPS 接口一般不用额外处理。但本地开发用的是 HTTP就可能被系统网络策略拦下来现象就是SocketException或者Connection refused。别在 Dart 层疯狂改代码先查平台权限。最后把生成的lib/database目录提交进工程同时在main.dart里初始化void main() { final supabase SupabaseClient( https://your-project.supabase.co, your-anon-key, ); runApp(MyApp(supabase: supabase)); }到这里一个能跑在鸿蒙 Flutter 引擎里的强类型数据处理链路就通了。5. 常见问题与排查实录5.1 本地开发连不上 Supabase症状跑dart run supabase_codegen:generate时报连接超时或 401。排查路径先确认 Supabase 本地实例有没有起来supabase status看一下 API 和 Postgres 端口再检查service_role_key是不是当前项目的密钥别把anon_key复制上去。还有一种情况是公司内网代理把 54321 端口屏蔽了这时候直接用云端临时项目跑 codegen或者把 schema SQL 导出到本地文件配合 codegen 的离线 schema 模式。5.2 生成的代码在鸿蒙上运行时类型转换失败症状type _MapString, dynamic is not a subtype of type ListMapString, dynamic。这种通常是 codegen 版本和运行时supabase包的查询返回结构不一致。比如旧生成器把.select()返回结果当ListMapString, dynamic但新版本supabase会返回Listdynamic。解决办法不是手改生成代码而是把 supabase_codegen 和 supabase 升级到匹配的大版本然后重新生成。我曾吃过一次亏手动在模型层转了半天的类型后来发现是包版本不一致。5.3 枚举类型没有自动生成Supabase 里的CREATE TYPE枚举codegen 不一定默认生成。需要在配置里显式开启 types 生成有些版本还需要你在supabase.yaml里列出需要导出的 schematypes: - public如果开了还是没生成检查数据库角色能否访问 pg_type 表。权限不足时 codegen 拿不到枚举定义自然生成不出来。这时候与其去猜不如把登录用户换成postgres角色再跑一次。5.4 鸿蒙真机上请求被拦截症状代码没问题但supabase.from(UsersTable()).select()一直报网络错误。这个我排查得最久。一开始以为 DNS 问题后来发现是鸿蒙的网络权限没加。默认工程模板不一定包含INTERNET权限必须要手动在module.json5里加。如果用 HTTP 调试还会遇到明文流量限制。这种情况要么改用 HTTPS 代理要么在鸿蒙网络安全配置里临时放行调试域名。5.5 pub 依赖冲突症状flutter pub get报shared_preferences版本冲突或者url_launcher平台不支持。这套问题基本都出在全家桶插件上。我的建议是先把supabase_flutter从依赖里拿掉只保留supabase核心包。然后给网络、存储、深链这些能力单独接鸿蒙实现。如果你想省事可以先把依赖全部升级到最新版本再看.flutter-plugins-dependencies里是否识别到了鸿蒙平台识别不到就说明当前插件没有 ohos bridge 配置。5.6 代码生成结果在热重载后不刷新有时候你调整了数据库 schema重新跑 codegen但 IDE 里代码还是旧的。别慌这是 build 缓存问题。用dart run build_runner clean dart run supabase_codegen:generate如果改动频繁建议把 codegen 命令写进脚本和数据库迁移脚本绑定在一起执行。不然很容易出现数据库结构变了、生成代码没跟上前端就莫名其妙报错。6. 端云一体化数据处理体系的落地建议6.1 仓库层与服务层分离代码生成出来了但不意味着可以直接在 Widget 里到处调supabase.from(...)。我习惯再包一层 Repositoryclass UserRepository { UserRepository(this._db); final SupabaseClient _db; FutureListUser fetchActiveUsers() async { final data await _db .from(UsersTable()) .select() .eq(User_.status, active); return data; } }这样业务层不感知 database 细节未来如果要换本地数据库做缓存或者要加权限过滤都是在 Repository 这一层做。supabase_codegen 生成的强类型模型可以跨层复用但查询语句最好收敛到一个数据访问层。6.2 Realtime 与本地状态结合端云一体离不开实时数据。Supabase Realtime 底层是 WebSocket在鸿蒙 Flutter 里我用了web_socket_channel连接运行得很稳。配合上强类型模型收到推送后直接解析成 Dart 对象再丢给状态管理框架更新 UI。订阅代码大概长这样final channel supabase.channel(public:users); channel.onPostgresChanges( event: PostgresChangeEvent.all, schema: public, table: users, callback: (payload) { final newUser User.fromJson(payload.newRecord); // 更新本地列表 }, );这里有一个经验不要把所有表的变更都订阅鸿蒙真机上长连接多了很耗电。按需订阅当前页面需要关注的表离开页面时及时channel.unsubscribe()。6.3 增量生成与包体积控制如果你的数据库表特别多比如几十上百张表codegen 会把全部模型都生成出来这会让包体积变大编译时间也明显增加。可以在配置里用included_tables或exclude_tables来控制范围。我习惯只生成“真实业务表”一些日志表、内部关联表用手写模型替代。included_tables: - users - orders - products生成的代码里也会带一些不必要的辅助类建议在构建阶段开--tree-shake优化。对鸿蒙包体积本来就紧张的场景过滤表 压缩混淆能省不少空间。6.4 与 RLS 权限控制配套使用强类型映射解决的是开发效率不等于安全。Supabase 建议数据库层开启 Row Level Security所有客户端请求都要经过 RLS 策略过滤。使用 codegen 生成代码时它只会忠实反映 schema 结构不会帮你判断某条策略对不对。所以我把生成代码当作“数据访问的语法层”真正的权限判断依然放在 PostgreSQL 里。两层各司其职开发和安全性都不牺牲。7. 最后的实际操作体会这套适配做完我最大的感受是codegen 这类工具真正的投资回报不在生成代码量而在“约束一致性”。只要数据库 schema 一改整个 Flutter 端所有不匹配的引用都会在编译期爆出来。这个收益在跨端场景尤其明显不用再靠人工维护 Android、iOS、鸿蒙三份手写模型了。最后再分享一个小技巧我跑 codegen 的时候不会直接指向生产数据库。先本地起一套 Supabase 容器把迁移脚本在里面跑完确认 schema 正确后才生成代码。生成完顺手在本地例子上做冒烟测试全部通过再提交。这样既不会把半成品 schema 暴露到生产环境又能在出问题时快速回滚。鸿蒙端的 Flutter 项目现在越来越多人在做工具链也在快速完善但数据这一层早点走上强类型自动化后面会省很多事。