在 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;
  });
}