Skip to content

Flutter 离线写入队列:草稿、重试与冲突怎么处理

离线优先不等于“失败后一直重试”。用户在地铁里编辑一张工单,应用重启后仍应看见自己的修改;恢复网络时,同一操作不能提交两次;如果另一台设备已修改工单,还要给用户一个可理解的冲突状态。

把本地记录当作主视图 ​

UI 先从本地数据库读取,写入时在一个事务里同时更新可见记录和待同步队列。队列项至少保存 operationId、实体 ID、基础版本、变更内容、创建时间及状态。operationId 也发给服务端作为幂等键;超时后的重试才不会生成重复工单。

text
用户编辑 → 本地事务写入草稿 + pending 操作 → UI 显示“待同步”
网络可用 → 按实体串行提交 → 服务端确认版本 → 本地标记 synced
版本冲突 → 标记 conflict → 提供查看差异 / 重新提交入口

队列要允许单项失败,不能因为第一条格式错误就堵住所有工单。网络错误退避重试;权限失效暂停并等待重新登录;服务端明确拒绝时保留本地草稿并展示可操作原因。

冲突策略按字段决定 ​

备注可以考虑追加式合并,金额和审批结果通常需要服务端裁决,不应使用“最后写入覆盖”。同步完成后用服务端返回的最终实体替换本地快照,并清理已经确认的操作。后台任务在移动系统中不保证持续运行,因此前台恢复和用户主动同步都应能推进队列。

测试时覆盖杀进程后恢复、重复回执、两台设备同时编辑和权限过期。只测断网期间能显示草稿,无法证明同步安全。

一条工单从编辑到确认 ​

先定义本地事务:用户点击保存时,把工单草稿和 pending_operation 一起写入数据库。操作记录至少包含稳定的 operationId、目标 ID、baseVersion、序列化后的变更、创建时间与状态。草稿立即可见,但标记为“待同步”;不要先发 HTTP 再保存草稿,否则进程被系统回收时会出现服务端已处理、本地却毫无记录的断层。

例如列车驶入隧道时发出请求,服务端实际创建成功,客户端却没收到响应。重试必须使用同一个幂等键。服务端也必须在幂等窗口内保存键与结果,客户端单方面生成 UUID 并不能防重。收到确认后,在本地事务中更新服务端版本、正式实体快照和操作状态。若崩溃发生在响应到达后、确认写盘前,下次用原键重试并取得同一结果。

text
local draft + operation(id=K, base=7)
  → POST /work-orders  Idempotency-Key: K
  → timeout: keep pending, retry K
  → 200(version=8): atomically replace snapshot and acknowledge K
  → 409(current=9): stop this entity's queue and surface a conflict

失败要分流,不能只做指数退避 ​

网络超时和 5xx 可以退避重试,401 应暂停队列并等待重新认证,403 或校验错误应转为可处理的失败,409 应保留本地改动并展示字段差异。不同实体可以独立推进;同一实体的操作则必须按因果顺序执行,避免“修改标题”先于“创建工单”到达。若用户撤销一条尚未发送的操作,先确认是否存在后续依赖,再决定合并或取消。

验收时在发送前、服务器处理后但回包前、回包后但落盘前分别杀进程;再用两台设备制造版本冲突。每次都检查列表、待同步角标、服务端实体数和操作表,不能只看界面能否显示草稿。

参考:Flutter 离线优先架构。

MIT Licensed