Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

Flutter本地存储之Hive

由于floor不支持 Web 平台,所以就需要寻找在Web平台上的数据库替代方案。然后发现了 Hive,它是一个开源的 Flutter 数据库框架,它支持在全平台上运行且性能很好。虽然不是关系型数据库,只是轻量级的键值对数据库,也能满足一些常用的功能。

安装

1
2
3
4
5
dependencies:
flutter:
sdk: flutter
hive: ^2.2.3
hive_flutter: ^1.1.0

然后运行flutter pub get就能把Hive集成到项目中。

架构组件

  • Box:主要有BoxLazyBox两种,区别就是LazyBox不会把数据库的values全部加载到内存,而是在需要时才去本地数据库查询。对于Web平台每一个box都对应一个IndexedDBdatabase,其他平台的话box就是一个存放在给定目录的文件。
  • BoxCollections:支持同时打开或关闭一组普通Box的集合而且在 Web 平台上IndexedDB存储数据更高效。提供了对事务的支持且在Web平台上更高效。
  • Object:就对应数据库中的Entity(实体),Hive 可以存储绝大多数的数据类型,例如:Box.add('Dog')Box.put('name', 'Jack'),如果需要存储复杂的数据,就需要自定义一个对象,通常对象需要继承自HiveObject
  • TypeAdapter:是自定义对象的适配器,需要实现 typeId(全局唯一且范围(0,223))属性、read() 和 write() 方法。

支持自定义类型

自定义对象的支持主要有两种方式:

1.实现TypeAdapter然后在openBox之前注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import 'package:hive/hive.dart';
// 声明自定义类型
class User {
String name;

User(this.name);
}
// 声明自定义类型的适配器
class UserAdapter extends TypeAdapter<User> {
@override
final typeId = 0; // 全局唯一且范围(0,223),不然在openBox的时候会报错。

@override
User read(BinaryReader reader) {
return User(reader.read());
}

@override
void write(BinaryWriter writer, User obj) {
writer.write(obj.name);
}
}
// 使用Box之前先注册Adapter
Hive.registerAdapter(UserAdapter());
var box = await Hive.openBox<User>('userBox');
box.put('david', User('Jack'));
2.使用注解自动生成 TypeAdapter
  • @HiveType 标注自定义类并提供一个 typeId 属性
  • @HiveField 标注自定义类的属性,提供一个范围(0,225)的 index 且每个属性的 index 在当前类唯一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:hive/hive.dart';

part 'person.g.dart';

@HiveType(typeId: 1)
class Person {

@HiveField(0)
String name;

@HiveField(1)
int age;

@HiveField(2)
List<Person> friends;
}

然后运行dart run build_runner build就能自动生成 PersonAdapter 了。
使用这种方式,在更新Person类时不要更改字段的@HiveField 里的 index 和字段的类型,新增字段只要添加@HiveField 新的 index 即可,除非确认字段不再使用才能从类中删除该字段。如果一个字段不为空,在启用空安全后要提供字段的默认值。

3.HiveObject 的作用

当您在 Hive 中存储自定义对象时,您可以继承 HiveObject 来轻松管理您的对象。HiveObject 提供对象的typeId和有用的辅助方法,如 save() 或 delete()。这时候@HiveType 注解就不需要提供 typeId 属性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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); // 存储Person对象到Hive

print('Number of persons: ${persons.length}');
print("Lisa's first key: ${person.key}");

person.name = 'Lucas';
person.save(); // 更新Person对象
person.delete(); // 从Hive的Box中删除Person
print('Number of persons: ${persons.length}');

persons.put('someKey', person);
print("Lisa's second key: ${person.key}");
}
// 注解和自定义Adapter任选其一
@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);
}
}

实际运用

1.首先是实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hive/entity/person.dart

import 'package:hive/hive.dart';
part 'person.g.dart';

class Person {
final String id;
final String name;
final String nickName;
DateTime? dateCreated;
DateTime? dateModified;
final bool isFriend;

Person(this.id, this.name, this.nickName, this.dateCreated, this.dateModified, this.isFriend);

// 这里用了JsonSerializable用来序列化和反序列化
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

Map<String, dynamic> toJson() => _$PersonToJson(this);
}

这里的自定义对象我没有使用注解的方式,而是直接使用了自定义的 TypeAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hive/adpater/person_adapter.dart

import 'package:hive/hive.dart';

class NullablePersonAdapter extends TypeAdapter<Person?> {
@override
int get typeId => 0;
@override
Person? read(BinaryReader reader) {
String personString = reader.readString();
Map<String, dynamic>? personJson = personString.isNotEmpty ? json.decode(personString) : null;
return personJson != null ? Person.fromJson(personJson) : null;
}

@override
void write(BinaryWriter writer, Person? obj) {
String? personString = obj != null ? json.encode(obj.toJson()) : null;
writer.writeString(personJson ?? '');
}
}
2.其次是 Box 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// hive/box/person_box.dart

import 'package:hive/hive.dart';

abstract class PersonBox {
static String boxName = 'persons';
Future<void> savePerson(Person person);
Future<Person?> findPersonById(String personId);
void closeBox();
}

@Injectable(as: PersonBox)
class PersonBoxImpl extends PersonBox {
Box<Person>? _cachedBox; // 参考Hive框架的做法,打开一次后就把Box给缓存起来
final LocalStorage _localStorage;
PersonBoxImpl(this._localStorage);

@override
Future<void> savePerson(Person person) async {
Box<Person> innerBox = await _getCachedBox();
await innerBox.put(person.id, person);
}

@override
Future<Person?> findPersonById(String personId) async {
Box<Person> innerBox = await _getCachedBox();
return Future.value(innerBox.get(personId));
}

@override
void closeBox() {
_cachedBox?.close();
_cachedBox = null;
}

Future<Box<Person>> _getCachedBox() async {
return _cachedBox ??= await Hive.openBox<Person>(PersonBox.boxName, collection: _localStorage.userId);
}
}

3.最后是 HiveManager 聚合类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// hive/hive_manager.dart

import 'package:hive/hive.dart';

class HiveManager {
static String hiveBoxName = 'hive-database';
// 提供一个静态方法来初始化,方便其他地方调用
static Future<HiveManager> initInstance(PersonBox personBox) async {
_registerClassAdapter();
// init hive
await Hive.initFlutter();
HiveManager hiveManager = HiveManager(personBox);
return Future.value(hiveManager);
}

static void _registerClassAdapter() {
Hive.registerAdapter<Person?>(NullablePersonAdapter());
}

final PersonBox _personBox;
const HiveManager(this._personBox);

PersonBox get personBox => _personBox;

void closeBox() {
personBox.closeBox();
}
}

目前遇到的问题

  • 目前开发只在 Web 平台上运行,其他平台还没有试错。
  • Hive.openBox('name', collection: 'collectionName')如果打开多个Box使用同一个collectionName,除了第一个 Box 会打开成功并创建IndexedDBdatabase,后面的打开的Box并不会创建database,这就导致后续Box在操作数据的Future方法永远不会有返回值,程序运行到到方法里仿佛一直在等待一样。
  • 官方提供了BoxCollections来打开多个Box的集合,但是使用collectionopenBox的话,虽然会在同一个collectionName下创建多个 IndexedDBdatabase,但是用这些CollectionBox操作数据库会报错。