1. 课程导入与离线场景分析

在移动应用开发中,“联网”往往被默认为常态,但真实用户所处的环境远非如此理想。地铁隧道、电梯、地下车库、飞机客舱、偏远山区,甚至只是商场里一次短暂的信号拥塞,都会让应用瞬间失去网络。如果一款 Flutter 应用在断网时只能显示空白页或无限转圈,用户会直接判定它“坏了”。本地存储与离线工程要解决的核心问题,就是让应用在网络不可用时依然可用、可读、可写,并在网络恢复后把数据安全地同步回服务端。

本课程的学习目标可以归纳为三层。第一层是数据落地能力:掌握在 Flutter 中把结构化数据、文件、键值对持久化到设备本地的多种手段,理解它们各自的适用边界。第二层是离线可用能力:让界面在无网时仍能渲染上一次成功获取的数据,并允许用户产生新的本地操作。第三层是同步一致性能力:在网络恢复后,把本地变更按正确顺序、正确冲突策略合并到远端,避免数据丢失或重复。三层能力层层递进,缺一层都会让离线体验出现明显裂缝。

离线场景的三种典型形态

离线并不是一个单一状态,按网络质量与用户行为可以拆成三类典型场景,它们的工程重点完全不同。

  • 完全无网环境:设备长时间没有可用网络,例如野外作业、飞机模式。此时应用必须完全依赖本地数据运行,重点是本地数据的完整性与可写性,用户产生的记录要先存本地,等待未来某次联网再上传。
  • 弱网同步:网络存在但极不稳定,延迟高、丢包多、频繁切换。此时请求可能超时或半途中断,重点是超时控制、重试策略与幂等设计,避免一次操作在服务端被执行多次。
  • 数据缓存:网络正常但用户希望减少流量、加快首屏。此时重点是缓存有效期、缓存淘汰策略与“先展示缓存、再静默刷新”的体验设计。

这三类场景对存储介质的要求并不相同。缓存类数据通常可以随时丢弃,适合放在内存或轻量键值库;用户产生的待同步数据则必须可靠落盘,不能因为应用被杀掉而丢失,需要事务型数据库或文件系统来保证。

核心技术挑战

把数据存到本地只是第一步,真正的难点在于“本地与远端如何保持一致”。第一个挑战是冲突处理:同一份数据在本地被修改、在远端也被别人修改,合并时以谁为准?常见策略有“最后写入胜出”“服务端优先”“客户端优先”以及字段级合并,选择哪种取决于业务语义。第二个挑战是同步顺序:本地可能积累了几十条待上传操作,如果它们之间存在依赖(先创建订单、再修改订单),乱序上传会导致服务端状态错误,因此需要为每条操作记录序号或时间戳,按序重放。第三个挑战是幂等性:弱网下客户端无法确定请求是否到达服务端,重试是必须的,但重试必须保证同一操作不会产生两份数据,通常通过客户端生成的唯一操作 ID 来实现。第四个挑战是数据迁移:应用版本升级后本地表结构可能变化,需要设计版本号与迁移脚本,否则老用户升级后会直接崩溃。

判断一个离线方案是否合格,可以问三个问题:断网时用户能否完成核心操作?恢复网络后数据能否自动补传?重复提交会不会产生脏数据?三个都能回答“是”,方案才算闭环。

整体技术选型地图

Flutter 生态中可用的本地存储方案大致分为四类,它们不是互相替代,而是各司其职、常常组合使用。

方案典型库数据形态适用场景
键值存储shared_preferences字符串、布尔、数字配置项、登录令牌、简单开关
关系型数据库sqflite / drift表、行、事务结构化业务数据、待同步队列
NoSQL 文档库Hive / Isar对象、集合高频读写、对象缓存、离线列表
文件存储path_provider + dart:io二进制、文本文件图片、音频、导出文件、日志

选型的基本判断逻辑是:数据量小且结构简单,用键值存储;需要复杂查询、事务和关联,用关系型数据库;以对象为单位高频读写、追求速度,用 NoSQL 文档库;非结构化的大文件,用文件存储。实际项目中,一个应用往往同时使用其中两到三种。

Flutter 离线工程:场景 → 挑战 → 选型 离线场景 完全无网 本地可读可写 弱网同步 超时·重试·幂等 数据缓存 有效期·淘汰 核心挑战 冲突处理 谁为准 同步顺序 按序重放 幂等性 操作唯一 ID 数据迁移 版本号·脚本 技术选型 键值存储 shared_preferences 配置·令牌·开关 关系型数据库 sqflite / drift 事务·待同步队列 NoSQL 文档库 Hive / Isar 对象缓存·高频读写 文件存储 path_provider 图片·音频·日志 离线闭环:本地写入 → 队列暂存 → 网络恢复 → 按序重放 → 冲突合并 → 状态回写 任一环节缺失,都会导致数据丢失、重复或界面与真实状态不一致

一个可参考的选型判断流程

面对一个新需求,可以按下面的顺序做判断,避免一上来就选最重的方案。

  1. 这份数据是否允许丢失?允许丢失的缓存类数据,优先用内存或 Hive,追求速度。
  2. 是否需要按条件查询、排序、关联?需要则选 sqflite 或 drift,用 SQL 表达查询逻辑。
  3. 是否需要在断网时写入并保证不丢?需要则必须落盘,且写入要放在事务里,保证要么全成功要么全回滚。
  4. 数据量是否超过几万条、是否包含大字段?超过则考虑分表、分页或改用文件存储,避免一次性加载拖垮内存。
  5. 是否需要跨版本升级表结构?需要则从第一版就引入 schema 版本号,为后续迁移留出空间。

以“离线待办清单”为例:任务列表用 sqflite 存表,字段包括 id、title、done、updated_at、sync_state;用户偏好(如默认排序)用 shared_preferences;每条本地修改在写入任务表的同时,向 sync_queue 表插入一条操作记录,包含 op_id、op_type、payload、created_at。网络恢复后,按 created_at 顺序读取 sync_queue,逐条发送,服务端以 op_id 去重,成功后删除对应队列项并把任务的 sync_state 置为已同步。这个结构同时覆盖了持久化、顺序与幂等三个挑战,是后续章节会反复用到的骨架。

不要把“离线支持”理解为简单地加一个缓存。缓存只解决“读”,而离线工程必须同时解决“读、写、同步、冲突”四件事。只做缓存的方案,在用户断网提交表单时依然会失败。

接下来的课程会沿着这条主线展开:先掌握各类存储介质的实际用法与性能特征,再构建可靠的本地数据模型与同步队列,最后处理冲突合并、迁移与测试。学完之后,你应该能够为一个真实的 Flutter 应用设计出完整的离线数据层,并说清楚每一个设计决策背后的取舍。