GSYFlutterBook/Z5.md

408 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<void> 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<double> 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<MyDatabase>(
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<RealmResultsChanges<Task>>(
stream: MyApp.allTasksRealm.all<Task>().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<Person>();
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<Person>
```
> 目前 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<Person> {
@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 工程或者框架的疑问欢迎留言评论这个系列是否更新就取决于是否还有新的素材