【Flutter Web】DriftでローカルDBを実装する
概要
Drift
「Drift」は、SQLiteを簡易的に使えるようにしたパッケージです。
このDriftを『Flutte Web』で使ってみたので、その備忘録を記す
「SQLite Wasm」 と 「Web Worker」
●SQLite Wasm
「WebAssembly」はウェブブラウザーでの処理をバイナリー形式で実行出来る仕組みです。
『SQLite Wasm』はSQLite公式のWebAssemblyです。
これを導入する事によりブラウザ上でRDBMSを実行出来るようになる訳です。
→ 本記事では「sqlite3.wasm」をダウンロードして使用します。
本記事でのDriftでは「IndexedDB」にアクセスさせます。
これにより、以下の事が可能となります。
・SQL操作
・大容量(数GB単位)のデーター保存
・永続化出来る(有効期限などの制限は無い)
●Web Worker
「Web Worker 」を使うと、Javascriptをマルチスレッドで実行することが出来ます。
DB処理を別スレッドで行うため、(時間が掛かっても)UI操作が固まる事を回避出来るようになります。
→ 本記事では「drift_worker.js」をダウンロードして使用します。
GitHub
●GitHub
本記事の全体的なソースコードはGitHubにて公開しております。
参考サイト
●参考になったサイト
実装
検証環境
※ 基本的なFlutter開発環境は整っている事を前提とします
プロジェクトを作成
では、早速やってみましょう。
●新規プロジェクトの作成
1.コマンドパレット(Command + Shift + P)を開き「flutter」と入力し、「Flutter: New Project」を選択。
2.一番上の「Application」を選択。
3.ワークスペースとなるディレクトリを選択(この中にプロジェクトが作成されます)
プロジェクト名は「note_flutter_drift」とします(GitHubのリポジトリ名もこれに合わせてます)。
下準備
●WebAssembly
先述の「SQLite Wasm」 と 「Web Worker」を使うための下準備をします。
まずは以下をダウンロードする
1.sqlite3の公式GitHubリリースページから「sqlite3.wasm」
2.driftの公式GitHubのリリースページから「drift_worker.js」
をダウンロードしてwebディレクトリに配置する。
※ 配置後は以下のような感じになります
●パッケージ
drift:Drift(DBパッケージ)
intl:ローカライズ関連。本記事では日時のフォーマット形式などで使用。
build_runner:build_runnerコマンド(ファイル名.g.dart)のコード生成
データーベース
database.dart を作成し、以下のようにします。
【database.dart】(とりあえず定義関連)
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart'; // Web専用(アプリで使うとエラーになるので注意)
part 'database.g.dart'; // ファイル名.g.dart
class Todos extends Table{
IntColumn get id => integer().autoIncrement()(); // ID
TextColumn get content => text()(); // 内容
// TextColumn get content => text().withLength(min: 12, max: 48)();
DateTimeColumn get createDatetime => dateTime()(); // 作成日時
// DateTimeColumn get createDatetime => dateTime().nullable()();
}
@DriftDatabase(tables: [Todos])
class Database extends _$Database {
Database._(QueryExecutor e) : super(e);
factory Database() => Database._(connectOnWeb());
@override
int get schemaVersion => 1;
}
DatabaseConnection connectOnWeb() { // DBコネクト(Web用)
return DatabaseConnection.delayed(Future(() async {
final result = await WasmDatabase.open(
databaseName: 'todos_db',
sqlite3Uri: Uri.parse('sqlite3.wasm'),
driftWorkerUri: Uri.parse('drift_worker.js'),
);
if (result.missingFeatures.isNotEmpty) {
print('Using ${result.chosenImplementation} due to missing browser '
'features: ${result.missingFeatures}');
}
return result.resolvedExecutor;
}));
}
「テーブル定義」「DB定義」「DBコネクト関数」を書きます。
connectOnWeb関数内のこの2行に注目してください。
先ほど下準備したファイルが使われてます。ここでデーターの保存先を指定している訳です。
※ 詳細は本記事の『データーの保存場所』にて後述
●「ファイル名.g.dart」のコード生成
$ flutter pub run build_runner build --delete-conflicting-outputs
build_runnerコマンドで「ファイル名.g.dart」のコード生成します。
今回の場合「database.g.dart」が生成されます
●CRUD用のメソッド
Databaseクラス内にCRUD用のメソッドを追記します。
DBへのアクセスは非同期で行うので、Future(ワンショット形式)やStream(ストリーム形式)となります。
Driftを使うとこんなに簡潔に書けちゃう
特に一番下のwatchEntries内で使用している「.watch()」は付けるだけでリアル監視出来ちゃうという優れもの。
【database.dart】(完成形)
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart'; // Web専用(アプリで使うとエラーになるので注意)
part 'database.g.dart'; // ファイル名.g.dart
class Todos extends Table{
IntColumn get id => integer().autoIncrement()(); // ID
TextColumn get content => text()(); // 内容
// TextColumn get content => text().withLength(min: 12, max: 48)();
DateTimeColumn get createDatetime => dateTime()(); // 作成日時
// DateTimeColumn get createDatetime => dateTime().nullable()();
}
@DriftDatabase(tables: [Todos])
class Database extends _$Database {
Database._(QueryExecutor e) : super(e);
factory Database() => Database._(connectOnWeb());
@override
int get schemaVersion => 1;
Future insertTodo(String content,DateTime createDatetime) {
return into(todos).insert(TodosCompanion.insert(content: content, createDatetime: createDatetime));
}
Future deleteTodo(int id) {
return (delete(todos)..where((todo) => todo.id.equals(id))).go();
}
Future updateTodo(int id, String content) {
return (update(todos)..where((todo) => todo.id.equals(id)))
.write(TodosCompanion(content: Value(content)));
}
Stream<List<Todo>> watchEntries() {
return (select(todos)).watch();
}
}
DatabaseConnection connectOnWeb() { // DBコネクト(Web用)
return DatabaseConnection.delayed(Future(() async {
final result = await WasmDatabase.open(
databaseName: 'todos_db',
sqlite3Uri: Uri.parse('sqlite3.wasm'),
driftWorkerUri: Uri.parse('drift_worker.js'),
);
if (result.missingFeatures.isNotEmpty) {
print('Using ${result.chosenImplementation} due to missing browser '
'features: ${result.missingFeatures}');
}
return result.resolvedExecutor;
}));
}
画面側
画面側を実装しましょう。
【main.dart】
import 'package:flutter/material.dart';
import './database.dart';
import 'package:intl/intl.dart'; // ロケール。日時表示で使う。
final db = Database();
void main() {
final list = Expanded(
child: StreamBuilder(
stream: db.watchEntries(),
builder:
(BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// snapshot.data![i].カラム名 では長いので
List<Todo> tl = snapshot.data ?? [];
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => ListTile(
leading: const Icon(Icons.person),
title: Text(tl[i].content),
//DateFormat df = DateFormat('yyyy-MM-dd HH:mm:ss');
subtitle: Text(
"[ID:${tl[i].id}] ${DateFormat('yyyy-MM-dd HH:mm:ss').format(tl[i].createDatetime)}"
),
trailing: Wrap(
spacing: 5, // アイコンの間の幅を調整
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
await db.updateTodo(tl[i].id,
'更新'
);
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async { // 削除なので、本来は「確認ダイアログ」を挟む
await db.deleteTodo(tl[i].id);
},
),
],
),
tileColor: Colors.purple[50], // アイテムの背景色
),
);
},
),
);
final addButton = ElevatedButton(
child: const Text('追加'),
onPressed: () async {
await db.insertTodo(
'追加', DateTime.now()
);
},
);
final body = SafeArea( // ボディー
child: Column(
children: [
list,
addButton,
],
)
);
final sc = Scaffold(
body: body, // ボディー
);
final app = MaterialApp(home: sc);
runApp(app);
}
main()直下にウィジェットを書きまくるという暴挙に出てますが、今回は検証用なのでご容赦を😉
・StreamBuilderのstreamに先ほどのwatchEntriesメソッドを指定。
・スナップショットとして、TodoクラスのListを指定。
の2点に注目
ちなみに「Expanded」でラッピングしてますが、これはListViewをColumnに入れる場合はExpanded等で制限をする必要があるからです。
動作確認
●ビルド
1.コマンドパレット(Command + Shift + P)を開き「flutter: Select Device」と入力
2.今回はWebなので「Chrome」を選択
「実行」ボタン押下でビルドします。
●動作確認
アプリ起動
「追加」ボタンを5回ほど押下
「ID:3」の編集アイコン押下
「ID:4」の削除アイコン押下
「localhost」の場合、ビルドは再インストールという形になります。
アプリでいう「終了 → 起動」のプロセスは、「更新」ボタンでブラウザを更新となります。
※ もちろん、外部へデプロイやリリースした場合は、ブラウザを閉じてもデーターは保存されます。
データーの保存場所
データーの保存場所(パス)についてですが
Web(ブラウザ)の場合、任意のファイルにアクセスは出来ません。
よってブラウザ固有のパスに保存されます。
例えばDrift(SQLite)であれば Storage - IndexedDB となります。
「表示」→「開発/管理」→「デベロッパーツール」
デベロッパーツールで「Application」→「Storage」→「IndexedDB」を表示
著書
【辛島信芳の著書】
IT技術などに興味のある方は、是非ご覧になってください。
この記事が気に入ったらサポートをしてみませんか?