IsarとriverpodでローカルDB管理
こんにちは、Flutterで個人アプリ開発をしている大学生です。数週間ほど前に新しいアプリをリリースしました。どんなアプリかというと、
自己管理ができなくて、だらけてしまう。。。
という人のためのゲーミフィケーション習慣化アプリです!
-
「勉強30分50円」のようにやるべきことに報酬を設定して、自己管理をする
-
「旅行40000円」のように、"ごほうび"を設定してお金が溜まったら換金できる
というコア機能を持っています。つまり・・・
RPGゲームのレベル上げをするような習慣化
を実現できます!!ぜひダウンロードしてみてください!
このアプリでは、ローカルDBに次々値を保存・編集していくような機能が必要でした。
そこでIsarというライブラリが見つかり、使ってみると結構便利で高速でした。
またIsarは開発されてからそこまで時間が経っていないので、Web上にあまり記事がありませんでした。
よって、この記事ではIsarの基本と実際の開発でありそうなシチュエーションである、Riverpodとの連携を解説していきたいと思います。
Isar Database
IsarはローカルDBを管理するパッケージです。Hiveというパッケージが先にあり、その後継となっています。
Isarはリレーショナルデータベースのsqfliteと異なり、キーバリュー形式でデータを保存するNoSQLです。Firebase Firestoreと同じような形式と言えます。
自分はまだ使ったことはありませんが、全文検索機能があるらしい!
また、Isarは優秀な日本語マニュアルがあるので、相当学習コストが低いと思います。
このドキュメントを読めば、一通りの実装方法はわかりますが、簡単に要約します。
Isarは以下の手順で実装できます。
ライブラリのインポート
flutter pub add isar isar_flutter_libs
flutter pub add -d isar_generator build_runner
データの定義
part 'email.g.dart';
class User {
Id id = Isar.autoIncrement; // id = nullでも自動インクリメントされます。
String? name;
int? age;
}
IDは自動インクリメントではなく、自身で指定することも可能。
Isarのクラスはコレクションと呼ばれる。(Firestoreと同じ)
またフィールドには以下のデータ構造が対応している。
- bool
- byte
- short
- int
- float
- double
- DateTime
- String
- List<bool>
- List<byte>
- List<short>
- List<int>
- List<float>
- List<double>
- List<DateTime>
- List<String>
また、この他にEnumも対応しています。これは使い勝手がいい!
コード生成
flutter pub run build_runner build
freezedのように --delete-conflicting-outputs
オプションを付けたほうが良いかもしれません。
Isarインスタンスの取得, CRUD
// 保存するパスの指定(パスを指定しなくても良い)
var path = '';
if (!kIsWeb) {
final dir = await getApplicationSupportDirectory();
path = dir.path;
}
isar = await Isar.open(
[EmailSchema,],
directory: path,
);
final newUser = User()..name = 'Jane Doe'..age = 36;
await isar.writeTxn(() async {
await isar.users.put(newUser); // 挿入と更新
});
final existingUser = await isar.users.get(newUser.id); // 取得
await isar.writeTxn(() async {
await isar.users.delete(existingUser.id!); // 削除
});
このようにIsarでは相当少ないコードで実装できていることがわかります。ここまでがIsarの基本的な使い方です。
Riverpodと一緒に使う
Flutterアプリの設計としてよく用いられているのがriverpodだと思います。そこで、IsarオブジェクトをStateNotifierでラップしてProviderで監視するような設計を考えてみたいと思います。ただし、Isarオブジェクトは公式ドキュメントと同じEmailオブジェクトとします。
Isarインスタンスをシングルトンにする
IsarインスタンスはSharedPreferencesのように1つのインスタンスを共有しておけば良いと考えて、シングルトンで実装します。
class MyIsar {
late Isar isar;
static final MyIsar _instance = MyIsar._internal();
factory MyIsar() {
return _instance;
}
MyIsar._internal();
Future<void> init() async {
MyIsar._internal();
var path = '';
if (!kIsWeb) {
final dir = await getApplicationSupportDirectory();
path = dir.path;
}
isar = await Isar.open(
[EmailSchema],
directory: path,
);
}
}
IsarオブジェクトのリストをStateNotifierでラップしてメソッドを定義
class EmailListState extends StateNotifier<List<Email>> {
EmailListState(List<Email> emailList) : super([]);
final Isar isar = MyIsar().instance.isar;
Future<void> getAll() async {
final list = isar.emails;
final getList = await list.where().findAll();
state = getList;
}
Future<void> add(Email item) async {
final list = isar.emails;
await isar.writeTxn(() async {
await list.put(item);
});
await getAll();
}
Future<void> deleteAt(int id) async {
final list = isar.emails;
await isar.writeTxn(() async {
await list.delete(id);
});
await getAll();
}
Future<void> deleteAll() async {
final list = isar.emails;
await isar.writeTxn(() async {
await list.where().deleteAll();
});
await getAll();
}
}
Providerを定義
final emailListStateNotifierProvider =
StateNotifierProvider<EmailListState, List<Email>>(
(ref) => EmailListState([]),
);
main関数内でIsarを初期化しておく
void main() {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
await MyIsar().init();
runApp(const ProviderScope(
child: MyApp(),
));
}
以上の方法でIsarオブジェクトを監視することができました。addメソッドなどを実行するとローカルDBが変更され、変更されたローカルDBを再取得し、自動的にProviderが更新されるという設計になっています。よってUI - state - ローカルDB 間に単方向のデータフローが保たれるというメリットがあります。
Discussion