# Flutter 工程化框架选择 — 搞定数据存储选型 ``` 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! ``` 这是 《Flutter 工程化框架选择》 系列的第三篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,可以说这是一个指引系列,所以比较适合新手同学。 > **Flutter 上关于数据存储或者数据库详细选型介绍的内容很少,也算是一个补全吧**。 本篇主要介绍数据存储相关,可能就有人会觉得,数据存储有什么好说的?不就是写个 Plugin 接个原生数据库就好了吗?这都能水? 确实,最简单快捷的方法就是写个 Plugin ,通过 Dart 直接调用原生平台的数据存储能力,这样实现成本最低,但是对于 Flutter 来说可能不够优雅: - 通过 `MethodCallHandler` 调用的方式,中间过程存在一定程度的性能消耗,特别是读写数据量比较大的时候 - Flutter 需要多平台支持,通过 `MethodCallHandler` 调用原生平台,就需要在每个平台的使用不同的代码和适配逻辑,会提高维护成本 **所以针对 Flutter 平台,现在社区的数据存储工具实现都默契的采用了另外一种方式**,当然不是说使用 `MethodCallHandler` 的 Plugin 实现不行,具体选型还是需要看你所需要的场景。 > Let's go 💗 ~ # Plugin 调用原生平台实现 首先简单介绍一下大家比较熟悉的两个数据存储的库,它们都是通过 Plugin 直接调用原生平台 API 进行数据存储: - **[shared_preferences](https://github.com/flutter/plugins/tree/main/packages/shared_preferences) key-value 的简单数据存储**,相信 Android 开发对这个名称会很亲切,官方维护,实现简单,支持全平台,适合对性能要求不高和数据量不大的场景 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image1.png) > 不过也是因为 `MethodCallHandler` 调用的方式,从维护和调用路径上会显得比较复杂,**federated plugin 的写法和版本管理方式容易出现问题**,已经不止一次在使用官方的 federated plugin 因为内部版本问题踩坑。 - [sqflite](https://github.com/tekartik/sqflite/tree/master/sqflite) 可以说是最早的第三方 Flutter 数据库之一,支持 iOS、 Android 和 MacOS ,为什么不支持全平台,**这其实也是 Plugin 直接调用多平台的问题之一,你需要重复在每个平台上实现同一个逻辑,虽然初期开发成本低,但是后续维护成本并不低**,同时 [sqflite](https://github.com/tekartik/sqflite/tree/master/sqflite) 提供的能力也比较弱,封装程度不高,主要还是解决了早期对数据库迫切支持的需求。 **既然说到 [sqflite](https://github.com/tekartik/sqflite/tree/master/sqflite) ,这里推荐一个 [flutter-sqlite-viewer](https://github.com/frgmt/flutter-sqlite-viewer) 的第三方工具**,相信大家在操作数据的时候都有可视化查看的需求,虽然开发过程中可以如下左图一样在 Android Studio 里通过 App Inspection 查看,但是有一些场景你没办法提供直连 Debug 调试,这时候 flutter-sqlite-viewer 就可以很方便提供本地化数据库查看的能力。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image2.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image3.gif) | | ----------------------------------------------------------- | -------------------------------------- | # sqlite3 从这里开始我们介绍不大一样的,[sqlite3](https://github.com/simolus3/sqlite3.dart/tree/master/sqlite3) 采用的是 `dart:ffi` 实现直接通过 Dart 访问数据库的能力,那这里的区别是什么? - dart 和 c/c++ 的相关代码可以跨平台,不需要维护多份同样的平台逻辑(虽然需要维护多份编译产物) - dart 和数据库直接交互,省去中间过程的转换需要 - **有问题了你不好调试** > 其实从 Flutter 2.0 和 Dart 2.12 提供 FFI 支持开始,到现在的 Dart 3.0 正式版发布,官方一直都在完善 ffi 还有和平台语言直接交互的能力,例如在 [Dart 2.18 里 Dart 就支持与 Objective-C 和 Swift 直接交互](https://juejin.cn/post/7137874832988831751)。 目前 sqlite3 支持 Android、iOS、Linux、MacOS、Window 平台,并且在特定平台还提供了切换到 [SQLCipher](https://www.zetetic.net/sqlcipher/) 的支持: - Android 平台可以**依赖 `sqlite3_flutter_libs` 来集成最新的 sqlite3 版本,也可以通过依赖 `sqlcipher_flutter_libs` 来切换到 SQLCipher** - iOS 平台和 macOS 平台与 Android 类似 - Windows 和 Linux 平台可以依赖 `sqlite3_flutter_libs` 来集成最新的 sqlite3 版本 - Web 平台其实也支持,只是只支持在 WASM 模式下,也就是 `--web-renderer canvaskit ` 的时候使用,同时还需要一些额外的操作,例如把 `sqlite3.wasm` 放到 `web/` 目录下 > **当然其实 sqlite3 不只是支持 Flutter ,它的 ffi 也是 Dart 的能力,所以它也可以直接用在 dart server 上**。 所以在 sqlite3 里 Dart 就是通过 `dart:ffi` 直接和数据库交互,而 Plugin 里的内容更多只是一个初始化的作用,例如 `sqlcipher_flutter_libs` 的 Plugin 就是加载对应的 `sqlcipher.so` 。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image4.png) # Drift **可能是直接使用 sqlite3 还不够优雅,所以作者针对 sqlite3 又封装了一套 [Drift](https://github.com/simolus3/drift)** ,Drift 同样可以脱离 Flutter 运行,因为它底层 sqlite3 依然基于 `dart:ffi` ,支持 Android、iOS、macOS、Linux 、 Windows 和 web , **不同之处是它对事务、 migrations、复杂过滤、表达式、批量更新和 joins 等操作做了封装**,例如: - 根据注解生成映射代码 - 对查询结果根据 Stream 实现自动更新 - 更方便的管理 schema migrations 和 `CREATE TABLE` 例如你可以通过如下所示进行聚合查询,将多个数据结果合并到一起: ```dart Future countTodosInCategories() async { final amountOfTodos = todos.id.count(); final query = select(categories).join([ innerJoin( todos, todos.category.equalsExp(categories.id), useColumns: false, ) ]); query ..addColumns([amountOfTodos]) ..groupBy([categories.id]); final result = await query.get(); for (final row in result) { print('there are ${row.read(amountOfTodos)} entries in' '${row.readTable(categories)}'); } } Stream averageItemLength() { final avgLength = todos.content.length.avg(); final query = selectOnly(todos)..addColumns([avgLength]); return query.map((row) => row.read(avgLength)!).watchSingle(); } ``` 当然,这并不是最有趣的,**Drift 里最有意思的是提供了 sql 解析器和分析器**,所以你可以通过 sql 语句来生成 API,并且还会在构建时提供错误警告。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image5.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image6.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 这里顺便提一嘴,**在 Flutter 里通过状态管理工具往下传递 `database` 大家应该不陌生吧**, drift 官方同样建议使用 `provider` 或者 `riverpod` 往下传递 `database` 对象,例如: ```dart void main() { runApp( Provider( create: (context) => MyDatabase(), child: MyFlutterApp(), dispose: (context, db) => db.close(), ), ); } ``` 同时,针对 sqlite,作者还提供了相关的选择建议: - 如果你不怕麻烦,想更轻量级,那么可以直接使用 [sqlite3](https://github.com/simolus3/sqlite3.dart/tree/master/sqlite3) - 如果你只需要 Stream 实现自动更新,不需要自动生成 dart 查询映射,那可以选择 [sqlcool]([Homepage](https://github.com/synw/sqlcool)) - 如果你需要和 Drift 功能类似,但是需要更灵活的自定义能力且不介意麻烦的话,可以选择 [floor](https://github.com/vitusortner/floor) 最后,如下图所示,你还可以用 [db_viewer ](https://github.com/vanlooverenkoen/db_viewer) 来预览数据库内容数据。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image7.gif) # Realm 可能不少人对 [realm](https://github.com/realm/realm-dart) 还并不了解,第一次接触 realm 是在 2016 年开发 React Native 的时候,那时候 realm 几乎就是我首选的数据库,它作为 SQLite 的替代,采用的是 MongoDB 的数据库方案。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image8.png) 如今 realm 也开始支持 Flutter ,虽然还在 Beta(已经 Beta 挺久了),但是它同样采用了基于 `dart:ffi` 的实现,所以 realm 同样支持脱离 Flutter 纯 Dart 运行,并且支持 Android、iOS 、Linux、MacOS 、Windows 等平台运行。 > 关于 MongoDB 和 SQL 的差异对比这里就不说,主要就是关系型数据库与非关系型数据库的区别 那 realm 最大的特别之处是什么?**那就是它除了提供本地数据库能力之外,它还提供数据同步和后端存储能力**。 简单来说,就是 realm 的数据库支持实时同步,在此之前我设计的 React Native 项目里,就有基于 realm 很快就开发出一套支持数据备份和同步的聊天应用的场景。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image9.png) 简单介绍一下,就是在 realm 的后台服务上,主要需要通过定义 Schema Table 和 sync 权限,就可以通过 realm SDK 在启动时同步数据,并对数据进行实时同步,而在 realm 上,你可以通过 `Credentials` 和 `User` 来定义角色的读写权限和管理数据。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image10.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image11.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 如下图所示,开启 sync 之后,在 iPhone 模拟器上点击添加的任务,在 Android 模拟器上就会实时同步 iPhone 上对数据库的操作更改,并且 realm 的 sync 后台默认就支持集群服务,如果用户量不是特别大的情况下,拿来做聊天或者客服场景还是很可行的。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image12.gif) > [realm-dart-samples ](https://github.com/realm/realm-dart-samples)里同样提供了对应的例子,但是它的例子有问题,所以你需要自己注册 realm.io 上的服务,并且获取到 appId 后,替换 appId 并在 realm 后台创建自己的 Task 表,开启 sync 服务。 同时 realm 也提供 [realm-studio](https://github.com/realm/realm-studio) 支持数据库可视化的能力,当然,如果你使用了 realm 的数据同步服务,那么在 Atlas 上也可以实时看到对应的数据更新。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image13.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image14.png) | | ------------------------------------------------ | ----------------------------------------------------------- | 不过还是那句话,目前 realm 还在 beta ,例如: - 很多 API 上可以看到文档紧缺 - 不支持 reload ,sync 模式下一 reload 就crash | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image15.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image16.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 另外,比如我不说,你翻阅文档也很难找到 Flutter 上如何监听和同步查询结果变化,而其实这部分代码其实你只需要通过 `stream` 就可以接入实现。 ```dart StreamBuilder>( stream: MyApp.allTasksRealm.all().changes, builder: (c, s) { return Text((s.data != null) ? s.data!.results.length.toString() : "null", style: Theme.of(context).textTheme.headline4); }) ``` 所以目前在 Flutter 上,如果在其他平台之前用过 realm ,那可以试试;但是如果没有,建议先不躺坑,等正式版吧。 > 其实和 Realm 一样具有实时同步能力,同样是基于文件的非关系数据库,并且更稳定更可靠的数据库是 [firebase_database](https://github.com/firebase/flutterfire/tree/master/packages/firebase_database/firebase_database) ,不过周所周知的欢迎,国内基本不会选择它。 # ObjectBox [ObjectBox ](https://github.com/objectbox/objectbox-dart) 其实和 Realm 类似,也是 NoSQL 类型的数据库,同样是基于 `dart:ffi` ,支持 Android 、iOS 、Linux 、MacOS 和 Window,**号称在能耗和速度上有绝对的优势,同时因为是纯 Dart API,所以它完全不需要你熟悉或者学习 SQL 语法** 。 ```dart @Entity() class Person { int id; String firstName; String lastName; Person({this.id = 0, required this.firstName, required this.lastName}); } final store = await openStore(); final box = store.box(); var person = Person(firstName: 'Joe', lastName: 'Green'); final id = box.put(person); // Create person = box.get(id)!; // Read person.lastName = "Black"; box.put(person); // Update box.remove(person.id); // Delete // find all people whose name start with letter 'J' final query = box.query(Person_.firstName.startsWith('J')).build(); final people = query.find(); // find() returns List ``` > 目前 objectbox 支持 dart 、java、kotlin 、swift 甚至还支持 GO 和 Python 在使用体验上,ObjectBox 可能会更贴近 NoSQL 的操作习惯, 另外它也可以直接在服务端被使用,从官方提供的基准测试中看,ObjectBox 的整体性能确实很优秀(其中的 Hive 后面介绍)。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image17.png) > 对测试感兴趣的可以看 [objectbox-dart-performance](https://github.com/objectbox/objectbox-dart-performance) ,不要问我它是不是真的这么好用,因为我也没在生产项目上使用它。 ObjectBox 另外一个特点就是支持离线数据同步:[ObjectBox Sync](https://objectbox.io/sync/) ,和 realm 还有 firebase 类似,这其实已经成为 NoSQL 服务商都会提供的特色之一。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image18.png) ObjectBox Sync 离线同步的支持主要在:**当设备离线时数据操作会被保存在本地,当设备连接上网络时,数据会恢复同步**。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image19.png) > 抛开 sync 能力不谈,ObjectBox 作为本地数据库在性能和开发体验上真的很不错。 # Hive 前面介绍的都是比较重的数据库类型,那接下来介绍个轻量型的存储框架: [Hive ](https://github.com/hivedb/hive/tree/master/hive)。 **Hive 是一个纯 Dart 实现的轻量 key-value 数据库,主要通过 dart/io 对文件进行读写**,支持 Android 、iOS、Linux、MacOS、Window、Web 平台。 作为 key-value 数据库,它其实很适合用来替代 shared_preferences ,并且它支持使用 AES 进行加密,目前官方提供的基准测试数据上性能表现还不错(虽然数据一大可能就拉胯)。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image20.png) Hive 的使用介绍这里就不赘述了,因为真的很简单直接,在 Hive 里: - Box 就类似 SQL 里的表 - Object 就类似于数据库中的实体对象 - Adapter 可以用来做自定义对象的适配器,可以用于实现一些`read` / `write` 操作 ```dart import 'package:hive/hive.dart'; void main() async { Hive.registerAdapter(PersonAdapter()); var persons = await Hive.openBox('persons'); var person = Person() ..name = 'Lisa'; persons.add(person); // Store this object for the first time print('Number of persons: ${persons.length}'); print("Lisa's first key: ${person.key}"); person.name = 'Lucas'; person.save(); // Update object person.delete(); // Remove object from Hive print('Number of persons: ${persons.length}'); persons.put('someKey', person); print("Lisa's second key: ${person.key}"); } @HiveType() class Person extends HiveObject { @HiveField(0) String name; } class PersonAdapter extends TypeAdapter { @override final typeId = 0; @override Person read(BinaryReader reader) { return Person()..name = reader.read(); } @override void write(BinaryWriter writer, Person obj) { writer.write(obj.name); } } ``` **另外 Hive 也支持如 `Hive.openLazyBox` 进行懒加载和 `compactionStrategy` 压缩数据来针对大数据量进行优化**,但是正如作者在 [#782 ](https://github.com/hivedb/hive/issues/782) 介绍的,当数据超过一定数量时,比如 50,000 ,那从性能上还是使用 SQLite 更好。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image21.png) 当然,在 [#170](https://github.com/hivedb/hive/issues/170#issuecomment-573874355) 里有通过对 `isolate` 提供共享内存的支持来优化 Hive ,但是很明显这条路是走不通的。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image22.png) 另外目前针对 **Hive 好像没有比较合适的可视化工具,这也许会影响部分人在选型上的考量**,不过 [hivedb](https://docs.hivedb.dev/#/more/vscode-snippets) 在 VSCode 上提供了模版代码片段的支持,也算是具备另外一种“微弱”的优势。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image23.gif) > PS:其实你可以拿 hivedb 当简单状态管理用,并且 Hive 脱离 Flutter 放在 dart server 端也是可以的运行。 **同样支持 key-value 高效存储的还有 [MMKV](https://github.com/Tencent/MMKV/tree/master/flutter ) ,MMKV 同样是利用 `dart:ffi` 支持了 Flutter ,相信 Android 开发对它不会陌生, 另外 [GetStorage](https://github.com/jonataslaw/get_storage) 也是基于二进制文件的键值对存储,不过更“低能”一些**。 # isar **也许是因为觉得 Hive 不够优秀,或者是 Hive 不支持高级查询,所以作者后续新推了新的数据库框架: [isar](https://github.com/isar/isar/tree/main/packages/isar)** ,同样基于 `dart:ffi` ,支持 Android 、iOS、Linux、MacOS、Window、Web 平台。 **如果说 Hive 可以简单代替 shared_preferences ,那 isar 就更像是平替 sqlite 的数据库**,所以 isar 更像是为了解决 query 问题而做的 Hive2.0 ,例如: - 支持复合和多索引、查询修饰符、JSON等 - 多 isolate 支持 - 支持十万条记录存储在单个 NoSQL 数据库 和 Hive 相比 isar 功能更丰富,不再只是单纯的 key-value 操作,可以支持更复杂的 filter 等查询条件,从使用体验上更符合 NoSQL 的数据库能力。 ```dart await isar.writeTxn(() async { final idsOfUnstarredContacts = await contacts.filter() .isStarredEqualTo(false) .idProperty() .findAll(); contacts.deleteAll(idsOfUnstarredContacts); }); ``` 同时 isar 提供了强大的可视化工具 ,通过 Isar Inspector 也是在一定程度补全了 Hive 的不足,使用 Inspector 只需要在 open 时设置 `inspector: true` 就可以看到 ws 地址进行绑定访问。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image24.gif) 其实 isar 另外的好处就是完全开源,从构建脚本到逻辑代码都是开源,例如其中通过 [`Cargo.toml` ](https://github.com/isar/isar/blob/main/packages/isar_core_ffi/Cargo.toml) 编排 `isar_core_ffi `,然后在 github action 会在发布时通过 shell 脚本执行如何构建 isar 的动态库等。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image25.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image26.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 例如 isar 在 iOS 上最重接入的是 `isar.xcframework` ,在 Android 上是 `isar.so` | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image27.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image28.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 目前集成 isar 库本身并不是很大,例如在 Android 上集成之后,大小只增加了大概 600 多k ,所以作为 Hive 的升级版本,isar 确实值得一试,。 | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image29.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image30.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | > 目前由于 Isar Web 依赖于 IndexedDB ,所以可能和其他平台相比较存在一定限制。 最后我们看来自 [guide-to-isar](https://itnext.io/a-minimalist-guide-to-isar-ee43c1e51a85) 和 [flutter-db-benchmarks](https://github.com/msxenon/flutter-db-benchmarks) 的一份数据对比: - 左边的数据对比提供了在 50,000 条数据下 - 插入耗时 isar < ObjectBox < Realm - 删除耗时 isar < Realm < ObjectBox - 数据库大小 isar < Realm < ObjectBox - 条件查询 isar < Realm < ObjectBox - 右边的数据对比了在 10,000 条数据下 - 插入耗时 ObjectBox < isarAsync < isarSync < Hive - 读取耗时 Hive < ObjectBox < isarAsync < isarSync - 更新耗时 ObjectBox < isarAsync < isarSync < Hive - 删除耗时 ObjectBox < isarAsync < isarSync < Hive | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image31.png) | ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image32.png) | | ----------------------------------------------------------- | ----------------------------------------------------------- | 这两份数据虽然看起来有矛盾点,但是这其实和测试环境有关系: - 左侧的图片时在较为正常设备上的测试,可以视为一般情况 - 右侧的数据测试的是在低端设备上的表现,可以视为最坏的情况 就像 [#211](https://github.com/isar/isar/discussions/211) 里讨论的,同样的基准测试,作者在它的设备上测试反而 isar 表现更好,讨论的结论上看也是,**除了速度之外,在是否导致 UI 卡顿上 isar 的表现也更好,所以基准测试的覆盖范围也是一种考量**。 ![](http://img.cdn.guoshuyu.cn/20220928_Z5/image33.png) # 最后 最后,本篇介绍了 [shared_preferences](https://github.com/flutter/plugins/tree/main/packages/shared_preferences) 、 [sqlite3](https://github.com/simolus3/sqlite3.dart/tree/master/sqlite3) 、 [Drift](https://github.com/simolus3/drift) 、 [realm](https://github.com/realm/realm-dart) 、 [ObjectBox ](https://github.com/objectbox/objectbox-dart) 、 [Hive ](https://github.com/hivedb/hive/tree/master/hive) 和 [isar](https://github.com/isar/isar/tree/main/packages/isar) 等数据存储框架,简略带过的还有 [sqlcool]([Homepage](https://github.com/synw/sqlcool)) 、 [floor](https://github.com/vitusortner/floor) 、 [MMKV](https://github.com/Tencent/MMKV/tree/master/flutter ) 和 [GetStorage](https://github.com/jonataslaw/get_storage) 等,可以看到,**这份数据也侧面体现了目前 Flutter 生态的健全,至少在数据存储框架上就可以让人产生“选择困难症”** 。 如果你还有什么关于 Flutter 工程或者框架的疑问,欢迎留言评论,这个系列是否更新就取决于是否还有新的素材~