如何在 Flutter 使用本地端輕量資料庫?
在 Flutter中 ,有時候我們需要一個本地端的輕量資料庫做永久性的關係儲存,通常是簡單的物件對應關係。過往通常使用 sqlite,但印象中這在 Apple Store 上架時會造成問題,因為 Apple 不希望一個 App 包含兩個程式 (未查證來源)。
目前我查到 Flutter 比較多人用的是 Hive以 Apache 2.0 授權釋出。(於 2022/04/13 用 google search 查詢 "flutter hive" 跟 "flutter objectbox",分別是 1,950,000 33,500。)
雖然 ObjectBox 做了一篇比較,並認為讀取速度比 Hive 快,但我閱讀其文件後覺得它的使用過於複雜,不合我使用,因此本文將著重在說明 Hive 的用法:
什麼是 Hive?
var box = Hive.box('myBox');
box.put('name', 'David');
var name = box.get('name');
print('Name: $name');
Hive 是一個輕量化的 key-value pairing 資料庫。官方聲稱為「Fast, Enjoyable & Secure NoSQL Database」。
功能包括:
- 🚀 跨平台資源: mobile, desktop, browser
- ⚡ 效率比 Sqlite 快
- ❤️ 簡易使用的 API
- 🔒 Strong encryption built in
- 🎈 沒有 Native 相依性
加入到專案
套件有:
- Hive :: 本體
- hive_flutter:: 針對 flutter 的調整
- [[hive_generator]] :: 要用客製化資料型別才需要用到的 generator
全部加入,pubsepc.yaml 看來如下:
dependencies:
hive: ^[version]
hive_flutter: ^[version]
dev_dependencies:
hive_generator: ^[version]
build_runner: ^[version]
初始化
Hive 需要知道可以存取的目錄位置,Flutter 中不同作業系中的目錄位置不同,所以我們需要初始化它,hive_flutter
有提供 helper function initFultter()
可以使用。
用法如下:
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
await Hive.initFlutter();
await Hive.openBox('settings');
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
....
}
}
什麼是 Box?
Box 相當於 RMDB 裏的 table 但沒有 Schema,簡單的 App 通常只需要一個 box。 Box 甚至可以作加密以便儲存敏感資訊。
Get open box
會把資料讀到記憶體上
var box = Hive.box('myBox');
Close box
不需要時要關掉
App 關掉要做 close, 關掉所有的 box
Hive.close()
可以儲存 Object
@HiveType(typeId: 0)
class Person extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
}
Read & Write
操作上跟 Map 差不多,寫入的方式如下。寫入是在背景執行,所以不需要做 async。如果寫入失敗,listener 會拿到 old value。
如果想要確定 write 成功,可以使用 wait
var box = Hive.box('myBox');
box.put('name', 'Paul');
box.put('friends', ['Dave', 'Simon', 'Lisa']);
box.put(123, 'test');
box.putAll({'key1': 'value1', 42: 'life'});
LazyBox
lazy box 的行為會不太同, 操作失敗。put 會回傳 future 未完成, get 會回傳 old value 或是 null
var box = await Hive.openBox('box');
box.put('key', 'value');
print(box.get('key')); // value
var lazyBox = await Hive.openLazyBox('lazyBox');
var future = lazyBox.put('key', 'value');
print(lazyBox.get('key')); // null
await future;
print(lazyBox.get('key')); // value
而若想要對使用 Hive 的 Code 做 Unit Test,則官方文件建議如下:
[!quote] TLDR: if you want to unit-test your code, and just your code, mock Hive. If you want to test the entire login functionality as a unit, initialize Hive with a temporary database. But if you do this, be careful to have a brand new database on each test run, as failing to do so may render your tests unstable.
使用 Hive 這種相依到硬碟空間的資料庫需要設定暫存目錄。path_provider
需要做 mock,否則會報錯。
mock 方式如下:
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
setUpFixtures() {
TestWidgetsFlutterBinding.ensureInitialized();
const testFixturesDir = 'test/fixtures';
const channel = MethodChannel(
'plugins.flutter.io/path_provider_macos',
);
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return testFixturesDir;
});
}
setUpTmpDir() async {
TestWidgetsFlutterBinding.ensureInitialized();
final Directory tmpDir = await Directory.systemTemp.createTemp();
Hive.init(tmpDir.path);
const channel = MethodChannel(
'plugins.flutter.io/path_provider_macos',
);
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return tmpDir.path;
});
}