ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

riverpod_sqflite 实战指南:基于 SQLite 的 Riverpod 离线持久化完整实现

riverpod_sqflite 实战指南:基于 SQLite 的 Riverpod 离线持久化完整实现 前端移动开发【免费下载链接】riverpodA reactive caching and>项目地址https://gitcode.com/gh_mirrors/ri/riverpod点击查看免费下载导读riverpod_sqflite是 Riverpod 生态中官方提供的离线持久化实现它通过 SQLitesqflite为 Riverpod 的状态提供跨应用重启的持久化能力。本指南以packages/riverpod_sqflite/README.md为骨架结合 存储实现源码、核心持久化抽象 与 单元测试带你从零搭建storageProvider连接器、在AsyncNotifier中接入persist并深入理解缓存时间、销毁键destroyKey与数据库表结构等底层机制。读完你将掌握一套可直接复制的「读库恢复状态 状态变更自动写库」的完整离线缓存方案。什么是 riverpod_sqflite官方离线持久化适配层riverpod_sqflite版本 0.4.7见 pubspec.yaml是 Riverpod 官方对「离线持久化」的 sqflite 落地实现。它并非一个独立的状态管理方案而是一个Storage 适配器负责把 Riverpod 的状态以 JSON 形式写入 SQLite 数据库并在下次启动时读回。它的角色可以在 Storage 抽象 的文档注释中得到印证——Riverpod 核心库把「如何与数据库交互」抽象为StorageKeyT, EncodedT接口并明确说明「Storages are generally implemented by third-party packages. Riverpod provides an official implementation of [Storage] that stores data using SQLite, in theriverpod_sqflitepackage.」也就是说riverpod核心包只负责持久化流程编排何时读、何时写、何时删riverpod_sqflite负责具体的数据库读写如果你愿意也可以实现自己的Storage例如使用 Hive、SharedPreferences只需满足read / write / delete / deleteOutOfDate四个方法即可。从源码导出口 riverpod_sqflite.dart 可以看到该包对外只暴露一个类JsonSqFliteStorage。第一步创建数据库连接器 storageProvider按照 README 的用法首先需要创建一个通往数据库的连接器。官方推荐的做法是封装成一个FutureProviderJsonSqFliteStorage让所有需要持久化的 Provider 共享同一个存储实例final storageProvider FutureProviderJsonSqFliteStorage((ref) async { // Initialize SQFlite. We should share the Storage instance between providers. return JsonSqFliteStorage.open( join(await getDatabasesPath(), riverpod.db), ); });这段代码涉及两个关键点JsonSqFliteStorage.open(path)是唯一的构造入口构造函数本身是私有的。它在内部做了三件事见 riverpod_sqflite.dart调用openDatabase(path, version: 1, ...)打开或创建指定路径的 SQLite 数据库通过onCreate回调执行建表语句确保riverpod表存在调用deleteOutOfDate()清理所有已过期的数据。共享单例数据库连接应当全局唯一、跨 Provider 复用。注释 We should share the Storage instance between providers 明确强调了这一点——多个 Provider 各自 open 会产生多个数据库连接浪费资源且容易引发竞争。数据库文件的路径使用join(await getDatabasesPath(), riverpod.db)其中getDatabasesPath()来自 sqflite返回应用专属的数据库目录join来自package:path用于跨平台安全地拼接路径。完整示例可参考 example/lib/manual.dart。底层的表结构设计JsonSqFliteStorage在打开数据库时会创建一张名为riverpod的表源码 riverpod_sqflite.dartCREATE TABLE IF NOT EXISTS riverpod( key TEXT PRIMARY KEY NOT NULL, json TEXT, expireAt INTEGER, destroyKey TEXT ) WITHOUT ROWID四个字段的语义与核心库的PersistedData一一对应见 persist.dart字段类型含义keyTEXT主键持久化状态在数据库中的唯一标识由persist(key: ...)指定jsonTEXT经encode编码后的状态序列化内容expireAtINTEGER过期时间戳UTC 毫秒由cacheTime计算而来null表示永不过期destroyKeyTEXT数据销毁键用于强制作废旧数据见下文 destroyKey 详解WITHOUT ROWID是 SQLite 的优化选项由于key是主键且表结构紧凑该表可以直接以主键作为行存储减少一层索引开销。写入时使用ConflictAlgorithm.replace源码即「键已存在则整体覆盖」天然支持 upsert 语义。第二步在 Notifier 中 mix-in Persistable 并调用 persist数据库连接器就绪后接下来就是把某个 Notifier 的状态接入持久化。README 给出的核心范式是让AsyncNotifier在build方法开头调用persist。class TodosNotifier extends AsyncNotifierListTodo { override FutureOrListTodo build() async { // We call persist at the start of our build method. // This will: // - Read the DB and update the state with the persisted value the first // time this method executes. // - Listen to changes on this provider and write those changes to the DB. // We await for persist to complete to make sure that the decoding is done // before we return the state. // If you do not care about the decoded value, dont await the future. await persist( // We pass our JsonSqFliteStorage instance. No need to await the Future. // Riverpod will take care of that. ref.watch(storageProvider.future), // A unique key for this state. // No other provider should use the same key. key: todos, // By default, state is cached offline only for 2 days. // In this example, we tell Riverpod to cache the state forever. options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever), encode: jsonEncode, decode: (json) { final decoded jsonDecode(json) as List; return decoded .map((e) Todo.fromJson(e as MapString, Object?)) .toList(); }, ).future; // If a state is persisted, we return it. Otherwise we return an empty list. return state.value ?? []; } Futurevoid add(Todo todo) async { // When modifying the state, no need for any extra logic to persist the change. // Riverpod will automatically cache the new state and write it to the DB. state AsyncData([...await future, todo]); } }这段代码是离线持久化的「最小完整闭环」其核心机制需要拆解为四个层次1.persist的双向职责persist来自package:riverpod/experimental/persist.dart导出的NotifierPersistXmixin在首次执行build时做两件事读从数据库读取key: todos对应的历史状态解码后写入当前AsyncNotifier的 state写订阅该 Provider 的状态变化每次 state 更新时自动把新状态编码后写入数据库。这正是 README 注释中 Read the DB and update the state with the persisted value Listen to changes on this provider and write those changes to the DB 的完整含义。因此add(Todo)方法里只写了一行state AsyncData([...await future, todo])没有任何额外持久化代码——状态更新与落库是自动绑定的。2. 为什么persist传入的是.futureref.watch(storageProvider.future)得到的是一个FutureJsonSqFliteStorage而不是存储实例本身。README 注释说明 No need to await the Future. Riverpod will take care of that.——persist内部会自行等待存储就绪。这样storageProvider与其他 Provider 之间形成了自然的依赖图存储初始化顺序由 Riverpod 保证。3.await persist(...).future与return state.value ?? []的组合persist返回的 future 表示「解码完成」这一时刻。await它确保从数据库读回的状态已经合并进当前 state之后state.value才可靠README 注释特别提醒如果你不关心读回的解码值可以不 await例如仅在启动时静默恢复缓存return state.value ?? []是兜底逻辑数据库中有历史状态就返回它否则返回空列表作为初始数据。这里state.value只可能来自两种来源——persist刚写入的恢复值或上次 build 已计算的值。4. 序列化契约encode: jsonEncodeListTodo→ JSON 字符串decodeJSON 字符串 →ListTodo通过Todo.fromJson逐条还原key: todos必须是全局唯一键README 强调 No other provider should use the same key因为数据库表以key为主键撞键会导致状态互相覆盖。深入 StorageOptionscacheTime 与 destroyKeypersist的第三个参数options控制缓存生命周期策略其完整定义位于 StorageOptionsconst StorageOptions({ this.destroyKey, this.cacheTime const StorageCacheTime(Duration(days: 2)), });cacheTime默认缓存 2 天默认值是Duration(days: 2)即状态只在数据库里保留 2 天。过期数据的清理时机有两个见 persist.dart 的注释应用重启时JsonSqFliteStorage.open会先执行deleteOutOfDate过期后再次读取该 Provider 时。StorageCacheTime提供了两个构造形态源码const StorageCacheTime(Duration this.duration); // 自定义有效期 static const unsafe_forever StorageCacheTime._(null); // 永不过期StorageCacheTime(Duration(days: 3))自定义 3 天有效期StorageCacheTime.unsafe_forever永不过期。关于unsafe_forever核心库源码给出了重要的警告persist.dart不推荐无条件永久持久化。因为如果某天你从应用源码中删除了该 Provider旧用户的数据库里仍会残留它的数据且 Riverpod 不会提供任何清理工具——届时你必须自己写数据库迁移来删除这些孤儿数据。这正是它名字里 unsafe 的由来。README 的示例为了演示「缓存永远有效」而刻意使用了它实际项目请权衡取舍。destroyKey绕过复杂迁移的「状态作废开关」destroyKey是 README 未展开、但源码明确支持的高价值特性persist.dart当某个 Provider 的状态发生了破坏性变更如数据结构重构与其写复杂的数据库迁移不如在发布前修改该 Provider 的destroyKey。一旦destroyKey变化旧状态会被销毁新状态从头重建。使用要点该值应在应用重启间保持稳定强烈建议使用常量变更它即触发「旧数据作废」同时PersistedData会携带destroyKey元数据参与比较persist.dart在 SQLite 侧destroyKey被单独存入一列destroyKey TEXT写入时仅在非空时才落库riverpod_sqflite.dart。数据库读写与过期清理的底层实现JsonSqFliteStorage的四个核心方法完整覆盖了Storage抽象接口Storage 接口定义方法职责sqflite 实现要点见 riverpod_sqflite.dartopen(path)打开库、建表、清过期openDatabaseonCreate建表 启动即清理L31-L48read(key)按主键读取事务内query加limit: 1空结果返回nullL84-L96write(key, value, options)写入/更新insert配合ConflictAlgorithm.replace按cacheTime计算expireAtL99-L110delete(key)删除指定键delete按key ?条件删除L77-L79deleteOutOfDate()清理全部过期数据事务内先建表容错再delete where expireAt 当前时间L58-L74值得注意的实现细节过期时间统一使用 UTC 毫秒时间戳clock.now().toUtc().millisecondsSinceEpoch见 riverpod_sqflite.dart并依赖clock包取时间——这使测试可以借助 fake clock 模拟时间流逝deleteOutOfDate在事务里先执行建表语句再删除L60-L66这样即使表被外部意外删掉调用也不至于抛错读取时允许返回过期数据Storage.read 契约是否过滤由persist的上层逻辑决定存储层只负责「存」与「取」。测试如何验证基于 fake clock 与内存数据库仓库的 persist_test.dart 是理解上述行为的最佳佐证。它通过sqflite_common_ffi在桌面环境初始化 sqflitesqfliteFfiInit()databaseFactoryFfi并使用inMemoryDatabasePath跑内存库Clears expired keys on creation写入一个默认 2 天缓存的数据和一个 3 天缓存的数据用fakeAsync拨快 3 天时钟后再open一次数据库断言表内只剩maintained一条——精确验证了「过期数据在 open 时被清除」returns null on unknown keys/returns the value if it exists/returns null after a delete分别验证read的三种分支未命中返回null、命中返回PersistedDataString、删除后返回null。如果你想在真实设备/模拟器上跑这套测试只需在项目里添加sqflite_common_ffi作为 dev dependencypubspec.yaml 正是这么做的。进阶用 JsonPersist 注解配合代码生成README 展示的是手写persist的方式而仓库示例还提供了基于代码生成的等效写法example/lib/generated.dart。它与手写版的差别在于使用JsonPersist()注解标记 Notifier其定义位于 riverpod_annotation/experimental/json_persist.dart文档说明被注解的状态对象必须是原始类型int、String、bool、double、List、Map或实现了fromJson/toJson方法对的对象生成器会自动注入encode/decode逻辑因此persist调用不再需要手写encode:与decode:参数riverpod JsonPersist() class TodosNotifier extends _$TodosNotifier { override FutureOrListTodo build() async { persist( ref.watch(storageProvider.future), options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever), ); return state.value ?? []; } Futurevoid add(Todo todo) async { state AsyncData([...await future, todo]); } }两版示例对照阅读效果最佳手写版 manual.dart 展示了每个参数的显式含义生成版 generated.dart 展示了生产环境中更简洁的写法配合freezed定义Todo.fromJson。注意两版在persist的 await 处理上有细微差异README/手写版在build中await persist(...).future而生成版示例未 await——两者都合法区别仅在于「是否等到解码完成再返回 state」可按 README 注释的指引按需选择。依赖与集成清单在 Flutter 项目中启用riverpod_sqflite需要的最小依赖如下对应 pubspec.yamldependencies: riverpod: 3.4.3 # 提供 Storage 抽象与 persist mixin riverpod_sqflite: ^0.4.7 # 本适配包 sqflite: ^2.4.1 # SQLite 数据库驱动 path: ^1.8.0 # 路径拼接join/getDatabasesPath 场景其中riverpod_sqflite还间接依赖clock过期时间计算与meta注解支持。若采用代码生成路线还需追加riverpod_annotation、riverpod_generator与build_runner若想在本机桌面运行仓库内的 persist_test.dart则需在 dev dependencies 中加入sqflite_common_ffi。集成后的完整数据流可以概括为一条闭环应用启动 →storageProvider执行 →JsonSqFliteStorage.open()建表并清理过期行首个依赖持久化的 Notifier 执行build→persist读库恢复历史状态若有运行期state ...更新 →persist自动把新状态编码写入数据库下次启动重复第 2 步用户看到的是上次会话结束时的状态——即离线持久化的全部意义。赞分享前端移动开发【免费下载链接】riverpodA reactive caching and>项目地址https://gitcode.com/gh_mirrors/ri/riverpod点击查看免费下载相关推荐FlexSearch Node.js ESM 实战基于 SQLite 的 Document 持久化全文索引完整指南FlexSearch Node.js ESM 实战基于 SQLite 的 Document 持久化全文索引完整指南 FlexSearch 是一款面向浏览器与搜索引擎后端Mac QuickLook插件终极指南一键预览上百种文件格式Mac QuickLook插件终极指南一键预览上百种文件格式 你是否曾经在Finder中看到各种格式的文件却不知道里面装了什么Mac的QuickLook功文档TanStack DB 接入 SQLite 持久化基于 RxDB SQLite RxStorage 的原生与 WASM 存储实战TanStack DB 接入 SQLite 持久化基于 RxDB SQLite RxStorage 的原生与 WASM 存储实战 TanStack DB 是一数据库NoSQL嵌入式数据库实时数据库创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表