1. 课程导入与离线场景分析
在移动应用开发中,“联网”往往被默认为常态,但真实用户所处的环境远非如此理想。地铁隧道、电梯、地下车库、飞机客舱、偏远山区,甚至只是商场里一次短暂的信号拥塞,都会让应用瞬间失去网络。如果一款 Flutter 应用在断网时只能显示空白页或无限转圈,用户会直接判定它“坏了”。本地存储与离线工程要解决的核心问题,就是让应用在网络不可用时依然可用、可读、可写,并在网络恢复后把数据安全地同步回服务端。
本课程的学习目标可以归纳为三层。第一层是数据落地能力:掌握在 Flutter 中把结构化数据、文件、键值对持久化到设备本地的多种手段,理解它们各自的适用边界。第二层是离线可用能力:让界面在无网时仍能渲染上一次成功获取的数据,并允许用户产生新的本地操作。第三层是同步一致性能力:在网络恢复后,把本地变更按正确顺序、正确冲突策略合并到远端,避免数据丢失或重复。三层能力层层递进,缺一层都会让离线体验出现明显裂缝。
离线场景的三种典型形态
离线并不是一个单一状态,按网络质量与用户行为可以拆成三类典型场景,它们的工程重点完全不同。
- 完全无网环境:设备长时间没有可用网络,例如野外作业、飞机模式。此时应用必须完全依赖本地数据运行,重点是本地数据的完整性与可写性,用户产生的记录要先存本地,等待未来某次联网再上传。
- 弱网同步:网络存在但极不稳定,延迟高、丢包多、频繁切换。此时请求可能超时或半途中断,重点是超时控制、重试策略与幂等设计,避免一次操作在服务端被执行多次。
- 数据缓存:网络正常但用户希望减少流量、加快首屏。此时重点是缓存有效期、缓存淘汰策略与“先展示缓存、再静默刷新”的体验设计。
这三类场景对存储介质的要求并不相同。缓存类数据通常可以随时丢弃,适合放在内存或轻量键值库;用户产生的待同步数据则必须可靠落盘,不能因为应用被杀掉而丢失,需要事务型数据库或文件系统来保证。
核心技术挑战
把数据存到本地只是第一步,真正的难点在于“本地与远端如何保持一致”。第一个挑战是冲突处理:同一份数据在本地被修改、在远端也被别人修改,合并时以谁为准?常见策略有“最后写入胜出”“服务端优先”“客户端优先”以及字段级合并,选择哪种取决于业务语义。第二个挑战是同步顺序:本地可能积累了几十条待上传操作,如果它们之间存在依赖(先创建订单、再修改订单),乱序上传会导致服务端状态错误,因此需要为每条操作记录序号或时间戳,按序重放。第三个挑战是幂等性:弱网下客户端无法确定请求是否到达服务端,重试是必须的,但重试必须保证同一操作不会产生两份数据,通常通过客户端生成的唯一操作 ID 来实现。第四个挑战是数据迁移:应用版本升级后本地表结构可能变化,需要设计版本号与迁移脚本,否则老用户升级后会直接崩溃。
整体技术选型地图
Flutter 生态中可用的本地存储方案大致分为四类,它们不是互相替代,而是各司其职、常常组合使用。
| 方案 | 典型库 | 数据形态 | 适用场景 |
|---|---|---|---|
| 键值存储 | shared_preferences | 字符串、布尔、数字 | 配置项、登录令牌、简单开关 |
| 关系型数据库 | sqflite / drift | 表、行、事务 | 结构化业务数据、待同步队列 |
| NoSQL 文档库 | Hive / Isar | 对象、集合 | 高频读写、对象缓存、离线列表 |
| 文件存储 | path_provider + dart:io | 二进制、文本文件 | 图片、音频、导出文件、日志 |
选型的基本判断逻辑是:数据量小且结构简单,用键值存储;需要复杂查询、事务和关联,用关系型数据库;以对象为单位高频读写、追求速度,用 NoSQL 文档库;非结构化的大文件,用文件存储。实际项目中,一个应用往往同时使用其中两到三种。
一个可参考的选型判断流程
面对一个新需求,可以按下面的顺序做判断,避免一上来就选最重的方案。
- 这份数据是否允许丢失?允许丢失的缓存类数据,优先用内存或 Hive,追求速度。
- 是否需要按条件查询、排序、关联?需要则选 sqflite 或 drift,用 SQL 表达查询逻辑。
- 是否需要在断网时写入并保证不丢?需要则必须落盘,且写入要放在事务里,保证要么全成功要么全回滚。
- 数据量是否超过几万条、是否包含大字段?超过则考虑分表、分页或改用文件存储,避免一次性加载拖垮内存。
- 是否需要跨版本升级表结构?需要则从第一版就引入 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 应用设计出完整的离线数据层,并说清楚每一个设计决策背后的取舍。