brick_supabase 这个库圈内玩 Flutter 离线同步的基本都听过。它是 brick 生态里专门对接 Supabase 后端的适配器核心价值就一句话让你的应用在断网时照常写数据等网络恢复后再自动同步到云端全程不用你手写一条同步逻辑。这次我们要做的事是把这个库完整搬到鸿蒙生态里跑起来——不是简单换几个依赖重新编译而是从数据存储、平台通道到同步策略全部按鸿蒙的规则重做一遍。做鸿蒙化的第一步得先搞清楚 brick_supabase 在 Flutter 里是怎么工作的。它是典型的三层结构最上面是 Brick 提供的数据模型注解和仓库抽象中间是 Supabase 适配器负责 REST 请求和实时订阅底层是基于 sqflite 的离线缓存。这套设计在 Android 和 iOS 上跑了很久稳定性和社区认可度都不错但鸿蒙没有 sqflite也没有 Android 的那套 SharedPreferences更关键的是鸿蒙的 Flutter 引擎对平台通道的类型约定跟 Android 有细微差别。这些看似不起眼的差异在实际适配时会一个接一个地冒出来。我们团队从立项到跑通全量同步测试整整用了三周。这个过程中踩过的坑、验证过的方案、最后沉淀下来的适配路径我觉得值得写出来。这篇文章我会按整体设计、平台通道改造、实操步骤、问题排查、质量验证五条线展开尽量把我当时遇到的技术抉择和教训都交代清楚。内容会偏实践适合已经给 Flutter 应用做过鸿蒙化、或者正在研究离线同步方案的技术负责人和高级开发同学。1. 内容整体设计与思路拆解1.1 brick_supabase 的资产盘点到底要迁移什么动手之前先盘家底。brick_supabase 这个库由三部分资产构成缺一不可数据模型层基于 brick 的注解机制用ConnectOfflineFirstWithSupabase这类注解标记模型类自动生成离线同步所需的适配代码。这套代码生成机制跟平台无关是纯 Dart 层的东西理论上鸿蒙可以直接复用。同步引擎层核心逻辑全部封装在OfflineFirstManager里负责把本地未同步的操作排队、在联网后按序推送到远端同时处理远端拉取的数据落库。这层也是纯 Dart 实现依赖的dio、web_socket_channel都是跨平台的问题不大。存储与通道层这才是鸿蒙化的重头戏。存储用的是sqflite_common_ffiSQLite 的原生库通过 FFI 调用在鸿蒙上没有现成的动态库可用。其次是平台通道brick 通过MethodChannel调原生侧能力鸿蒙的 Flutter 引擎虽然支持这套机制但通道的实现姿态、返回类型约束跟前两者不完全一致。因此适配策略很清晰前两层尽量少动第三层全部重写。把sqflite换成鸿蒙的分布式键值库或关系型数据库把平台通道的宿主侧实现从 Android 代码换成鸿蒙的 ArkTS 代码。核心同步逻辑一行不改这样能最大程度复用官方测试用例把适配风险控制在底层。1.2 适配方案选型为什么我选了替换底层存储而不是引入中间层在适配思路上业内通常有两条路线方案 A引入 sqlite3 的鸿蒙编译版本继续用 sqflite 的 API 层改动量最小。方案 B直接用鸿蒙的关系型数据库RelationalStore重写存储适配器替换掉 sqflite。方案 A 听起来轻松实际复杂度极高。鸿蒙的 SQLite 需要自己用 NDK 交叉编译成 ohos 架构的动态库还要处理sqlite3_open、sqlite3_prepare这一层 FFI 的符号绑定。就算编译成功后续升级、多架构打包、崩溃符号解析都是体力活。更重要的是鸿蒙分布式能力——比如多设备协同、数据跨端流转——底层走的是自己的数据管理服务跟 SQLite 文件存储完全是两回事。如果只是用 SQLite 存一个本地文件等于放弃了鸿蒙原生数据库的分布式同步优势。方案 B 虽然需要重写一个存储适配器但接口范围是固定的sqflite 的 API 就那十几个方法。更关键的是鸿蒙的 RelationalStore 自带多设备数据同步能力后续如果你的应用要支持手机和平板协同同一套适配器直接能享受到这个红利。我最后选的方案 B重写SqliteStorage适配器为OhosRelationalStoreAdapter工作量和风险都可控。1.3 为什么值得做离线同步资产在鸿蒙上的三种典型价值砸三周时间做适配值不值我的判断是如果你手头有存量 Flutter 应用要上鸿蒙生态这件事是绕不开的。第一种价值叫资产复用。brick_supabase 里经过业务验证的同步策略、冲突解决规则、队列管理机制这些是花了大价钱调出来的逻辑。鸿蒙化之后原来积累的模型定义和仓库层代码可以平移到新平台不需要重新设计一套同步方案。第二种价值叫体验一致性。用户从 Android 换到鸿蒙设备数据能无缝迁移离线写入的体验没有断档。第三种价值叫降本。如果从零在鸿蒙上用 ArkTS 重写一套离线同步框架从设计、开发到稳定性验证周期至少两到三个月适配 brick_supabase 走的是已有的成熟路径验证重点聚焦在底层存储替换上。这些判断在后来的实操里基本都验证了。同步逻辑确实没动真正花时间的反而是存储适配和平台通道调试。2. 核心细节解析与实操要点2.1 离线优先同步的核心机制Batch 队列与 Delta 拉取要适配 brick_supabase必须先理解它的离线同步机制否则你连报错信息都看不懂。这套机制的核心是两件事写操作的本地队列和读操作的增量拉取。先说写操作队列。应用在离线状态下执行save时OfflineFirstManager并不会直接发网络请求而是把操作封装成Batch连同操作类型、目标表名、完整数据载荷一起写入本地队列表。队列的存储结构是 key-value 形式key 是自增 idvalue 是序列化后的操作对象。网络恢复后管理器按 id 顺序逐个取出来通过 Supabase 的 REST API 提交。这里有个细节brick_supabase 默认提交时会把请求包装成事务数组一次性发到 Supabase 的/rest/v1/rpc端点由服务端逐条执行并返回结果。哪条失败整个批次回滚失败原因记入日志表。再看读操作的 Delta 拉取机制。在线状态下适配器会按模型配置每 N 秒或者每次启动时拉取远端变更。它用的是 Supabase 的 Realtime 或 PostgreSQL 的变更订阅机制拉取增量数据的依据是服务端记录的updated_at时间戳对比本地缓存的最后同步时间只取差值部分。这个机制要求你的每张表必须有updated_at字段并且建立了索引否则查询性能会非常难看。理解了这两条主线你就知道适配时哪些东西不能动批次的序列化格式、队列的消费顺序、Delta 拉取的游标记录。这些都是跨平台无关的纯 Dart 层逻辑保持原样即可。2.2 平台通道改造鸿蒙宿主侧必须实现的五个接口brick_supabase 通过 MethodChannel 调用的原生能力集中在存储层实际梳理下来适配器依赖五个接口。这五个接口必须在鸿蒙宿主侧用 ArkTS 实现注册到同一个通道名上。init初始化数据库创建表结构返回当前本地 schema 版本。insert接收模型数据 JSON 和表名写入对应表返回自增 id。query接收查询条件表名、where 语句、limit、offset返回行数据 JSON 数组。update按主键更新数据行返回影响行数。delete按主键删除数据行返回影响行数。这里最容易踩的坑是通道参数类型映射。Flutter 侧的MethodCall参数是标准 Dart 类型经通道编码后鸿蒙侧接收到的 ArkTS 类型可能不是你想当然的那个。比如 Dart 的数字在通道里可能是num鸿蒙侧如果按number接收没问题但如果 Dart 侧传了int部分街边教程推荐的parseInt方式在字符编码上稍不留神就会解析失败。稳妥做法是在鸿蒙侧统一按string接收 JSON 字符串再JSON.parse解析成对象避免逐字段类型判断。数据返回同理。鸿蒙侧Result回调建议统一返回 JSON 字符串Flutter 侧用jsonDecode再转成业务对象。虽然多了一层序列化开销但换来的是跨平台行为完全一致排查问题更容易。2.3 存储层替换的风险控制数据格式兼容性设计上面说了方案 B要把 sqflite 换成鸿蒙 RelationalStore。但这里有个关键约束brick_supabase 的离线队列和模型数据是存在同一张 SQLite 表里的数据格式由 brick 的序列化机制决定。换存储引擎后数据到磁盘上的物理格式完全变了但读取和写入的逻辑要跟原库的语义保持一致否则同步队列会出现错乱。所以适配器内部我做了双表设计一张brick_metadata表专门存同步状态和队列元数据一张brick_models表存序列化后的模型数据。两张表都用了主键约束brick_models的逻辑主键是模型 id物理主键是自增rowid。这样既保证了 brick 侧通过 id 快速定位数据又能满足不同模型表名隔离的需求。云侧的数据结构不需要做任何改动。Supabase 的表结构、行级安全策略、实时订阅配置全部沿用之前的设定。这意味着后端资产完全复用鸿蒙端改造只发生在前端存储这一层。3. 实操过程与核心环节实现3.1 环境准备Flutter 鸿蒙引擎与依赖升级开始实操前环境准备是最耗时的环节。鸿蒙的 Flutter 引擎并不是社区维护的 Flutter SDK而是某个大厂在开源 Flutter 基础上做的分支版本对应的是 OpenHarmony 的 API 版本。我用的环境组合如下建议按这个基线来版本差太远会遇到各种莫名其妙的编译错误Flutter SDK基于 OpenHarmony 的 sdk 分支版本对齐 Flutter 3.19 的 API。DevEco StudioAPI 9 及以上配套的 ArkTS 编译工具链。brick 相关依赖brick_supabase升级到 3.0 以上版本sqflite_common_ffi锁在 2.3.0。鸿蒙侧依赖ohos.data.relationalStore和ohos.data.preferences这两个都是系统内置模块不需要额外引入。注意一个细节brick_supabase 的依赖树里默认带了sqflite在鸿蒙工程里直接依赖会在编译期报找不到原生库的错误。我当时通过 dependency_overrides 把sqflite_common版本固定同时不引入sqflite插件本体这样就避开了原生库编译的要求。3.2 核心改造一重写存储适配器这块是整个适配工作的核心。我写了一个OhosRelationalStoreAdapter类实现了 brick_supabase 要求的SqliteStorage接口。关键点有两个一是数据库初始化。鸿蒙的 RelationalStore 使用getRdbStore获取数据库实例需要传入配置对象包括数据库名、安全级别。我用的是应用私有目录的默认配置安全级别设为S1满足大多数业务场景。初始化时同步创建两张表SQL 语句跟在 SQLite 下几乎一样只需要把AUTOINCREMENT关键字换成鸿蒙支持的AUTOINCREMENT它确实支持并去掉WITHOUT ROWID这类 SQLite 专属语法。二是读写路径的类型转换。sqflite 的query返回ListMapString, Object?鸿蒙的ResultSet需要逐行遍历然后根据列类型取字符串或数字。我封装了一个resultSetToJson函数统一转成 JSON 字符串避免上层感知差异。写入时把 JSON 字符串直接作为TEXT类型存入对应列查询时原样返回。虽然多了一次序列化开销但同步逻辑完全不用感知存储层的变化。3.3 核心改造二平台通道宿主端实现存储适配器在 Dart 层直连 SQLite 是一条路但 brick_supabase 的部分能力还是通过 MethodChannel 走到原生侧的。像 SQFlite 的自定义函数、加密选项这类能力在鸿蒙上没有对应的原生实现所以通道的宿主端必须自己注册。我在鸿蒙工程里写了一个BrickStoragePlugin类实现Plugin接口重写onCallMessage方法。根据call.method分发到不同的处理函数。由于 Dart 侧传参和鸿蒙侧接收已经约定为 JSON 字符串处理逻辑非常简单基本就是解析、调用 RelationalStore API、返回 JSON。这段时间踩过最大的坑是通道名称不一致。brick 内部在MethodChannel(brick_supabase/storage)上通信但我在鸿蒙侧手滑写成了brick/storage结果所有通道调用直接超时报错信息是MissingPluginException。这类问题排查很费时间因为报错只出现在运行期编译完全正常。3.4 核心改造三离线队列与同步触发验证存储层替换完之后不能急着连 Supabase 正式库先在模拟环境里验证离线队列的完整链路。我搭了一个本地 Supabase 实例用 Docker 跑起来然后做了一组验证场景断网状态下新增 10 条数据确认全部进入本地队列表。恢复网络确认队列按序提交到 Supabase数据完整无缺。修改一条数据的updated_at字段触发 Delta 拉取确认只拉取变更行而非全表。杀掉应用进程再重启确认队列不丢失同步游标位置正确。这些场景跑通了才能说明底层存储替换没有破坏 brick_supabase 的离线同步语义。我在验证时还专门检查了微任务序列——brick 的队列消费用了Future链如果存储层某个方法抛了异常整个消费链会断掉表现就是队列头元素被无限重试。最后通过加try/catch和重试计数器解决。4. 常见问题与排查技巧实录4.1 问题一同步队列无限重试日志全是同一条记录现象应用联网后同步任务一直卡在队列头部日志里反复出现同一条待同步数据的记录没有任何进展。排查过程第一反应是网络请求失败但看了 Supabase 服务端日志请求根本没到。说明问题出在本地消费端。逐步打日志后发现问题在存储适配器的insert方法返回的自增 id 类型不对。鸿蒙 RelationalStore 的insert返回的是number而 brick 侧期望的是int的有效载荷。Dart 是强类型语言num和int在某些场景下能自动转换但在jsonEncode后会出现精度丢失最终导致队列标记失败。解决方案在返回前显式调用toInt()并在insert方法的返回值里统一封装成{id: id}结构与 sqflite 行为对齐。这个问题排查了整整一个下午最后定位到的原因简单到让人哭笑不得。这类问题我建议在编写适配器时就统一类型约束所有返回 id 的接口一律先转 Dartint再返回杜绝类型隐式转换。另外在适配器与 brick 核心层之间加一个简单的类型断言的 debug 校验开发期能在第一时间暴露问题而不是等到同步队列卡死。4.2 问题二Delta 拉取的游标丢失导致全量同步风暴现象每次应用启动都会触发一次全量数据拉取服务端压力大而且慢。查询本地日志发现游标记录——也就是last_synced_at——从未被正确写入。排查过程全量拉取说明 Delta 拉取的游标不存在。游标的存储位置我在改造时放到了鸿蒙的 Preferences 里。Preferences 在鸿蒙上是异步写入的flutter 侧如果不等写入完成就调用查询接口读到的永远是初始值。解决方案把游标写入改为同步等待在preferences.put后显式调用flush方法确保落盘完成再返回。同时把游标存储从 Preferences 迁移到了 RelationalStore 的brick_metadata表里统一走事务处理避免跨模块状态不一致。这个坑提醒我凡是核心状态数据尽量与业务数据放在同一存储引擎里用同一套事务来管。不要聪明地拆到多个存储那是给自己挖坑。4.3 问题三多模型关联同步死锁现象两个模型有外键关联离线状态下先后保存恢复网络后同步时出现外键约束冲突同步报错后队列阻塞。排查过程这是离线同步的经典问题。brick_supabase 的同步策略自带一个可配置项叫batchSize控制单次批量提交的记录条数。当父子模型在同一个批量里提交时如果父记录先执行但子记录引用的外键还没生成就会出现时序问题。解决方案方案分两层。第一层在模型关联中显式标注SupabaseSerializedField的关联顺序让同步引擎优先处理父模型第二层开启 brick_supabase 的Client级重试策略遇到外键冲突时自动重新排队。在实测场景里由于我们的业务模型大多是单表操作这类问题只在测试时才暴露但对复杂业务结构的应用来说这是必踩的坑。我建议在适配鸿蒙时专门留一周时间做并发触发测试重点盯多模型关联场景。4.4 问题速查表问题现象根因解决思路同步队列卡在头部存储适配器返回类型不正确统一返回 Dart int增加 debug 断言每次启动全量拉取Preferences 异步写入未落盘同步等待 flush或把游标迁移到业务库批量同步外键冲突父子模型提交时序错误配置关联顺序 开启重试策略通道调用 MissingPluginException通道名称不一致核对字符串常量统一用集中管理这个表我建议直接贴到团队文档里后续有人维护鸿蒙适配层时能省掉一轮轮的试错。5. 质量验证与上线自查清单5.1 离线优先全场景回归用例适配完成的唯一标准不是编译通过而是全场景回归用例跑通。我整理了一套适合离线优先场景的最小用例集覆盖了核心风险面冷启动断网写入 100 条数据强制杀进程后重装应用确认队列与数据不丢失。弱网环境下模拟 3G 网络断开与恢复交替进行 30 轮确认无死锁、无重复写。远端数据被另一客户端修改后本端同步确认拉取到最新值修改本端再次推送无冲突。同一模型在两个客户端同时修改确认冲突解决策略按预期执行brick 默认是 last-write-wins。这套用例我从头到尾用 shell 脚本编排断网操作通过后台切断模拟器的网络进程实现。全部跑完大概需要半天时间。5.2 性能基线与资源占用核查离线同步框架是常驻内存的性能基线必须卡好。我在真机上测了三组数据供你参考场景数据量冷启动耗时同步延迟本地队列恢复1000 条未同步记录198ms未同步前不阻塞 UIDelta 拉取变更 100 行46ms网络正常时 200ms全量拉取10000 行1.2s全量属于异常路径冷启动耗时主要花在数据库打开和表结构校验上。RelationalStore 的打开速度比 SQLite 慢 20% 左右但整体还在可接受范围。如果你的应用对冷启动时间要求极高可以考虑用延迟初始化策略把数据库初始化的时机从启动时延到首次需要读写时。这里需要收益取舍——如果延迟初始化同步队列的恢复时间会被推到后台用户看到的数据可能不是最新的。5.3 自查清单适配完成后逐项核对上线前建议把下面这份清单过一遍。每项都跟实际踩过的坑对应用文字版本留档后续其他项目做鸿蒙化时可以直接复用确认所有平台通道名称与 Dart 侧一致。全局搜索 MethodChannel 字符串逐一核对。确认存储适配器所有返回值显式做了类型转换。重点看 insert 和 query 两个方法。确认异步状态写入有同步阻塞或事务保障。重点看游标、队列进度等元数据。确认离线队列的序列化格式与原库一致。可以用统一构造的数据样本分别跑 sqflite 版和鸿蒙版对比输出 JSON 字节。确认同步触发逻辑包含重试和死锁保护。没有重试机制的同步框架在弱网环境就是炸弹。这份清单是我接手其他项目的移动端遗留问题时总结出来的每次做跨平台适配我都会从头到尾过一遍。它救过我很多次。6. 写在最后的一点实操体感回头复盘这三周的鸿蒙化适配我最想强调的一点是跨平台适配的难点从来不在写代码而在梳理清楚哪些该动、哪些坚决不能动。brick_supabase 的同步策略、队列机制、冲突解决规则是花了长时间验证的成熟资产这些属于不能动的部分。存储引擎、平台通道属于平台差异层必须按鸿蒙的能力重写。守住这条边界你就能把风险控制在一个很小的范围里。另一个体感来自测试策略。很多团队做鸿蒙化时报个“编译通过”就算完成了但离线同步这种长链路、多状态的框架真正的问题全部藏在运行期和异常路径里。我建议预算出至少 30% 的工期放在真机回归和弱网模拟上这笔钱花得最值。如果你手头也有基于 brick 的 Flutter 应用需要走上鸿蒙生态完全可以参考我上面这套路径。前面踩坑的部分我都写清楚了后面填土的事相信你比我更快。
阅读完成 · 觉得有帮助?