Flutterで内部データベースを扱うのに、良い方法ないかな?
Dartだけで、簡単に内部データベースを使いたいわ!
本記事では、そんな質問、要望にお答えします。
Flutterで内部データベースを簡単に扱えるようにするライブラリとして、
Driftを紹介します。
基本的な使用方法が理解できるように、
内部データベースからのデータの表示、追加、更新、削除のできる
サンプルコードで解説します。
内部データベースの選定に悩んでいる人には有用かと思います。
ぜひ読んでみてください!
Drift とは
「アプリを落としたらデータが消えてしまった!」
あなたはこのような経験はないでしょうか。
データの永続化を行っていないアプリを落とすと、
それまで記録されていたデータは削除されてしまいます。
Drift とは Dart/Flutterのアプリケーションでデータの永続化を行うためのライブラリです。sqflite
や sql.js のようなデータベースライブラリの上に構築されていて、
Streamでデータを受け取ったり、
SQLの知識が無くともDartのみでデータの追加や更新ができる機能を持っています。
特にDriftは、以下の時に有用なライブラリとなっています。
- データベース形式で内部にデータを保存したい
String
やBool
、Int
などの基本的な型しか取り扱わない
基本的な使い方
簡単なアプリの作成を通し、基本的な使い方を解説していきます。
上のgifで紹介しているようなアプリを作成します。
このアプリでデータの表示、追加、更新、削除を行うことができ、
アプリを落としてもデータが保存されていることがわかると思います。
準備
ベースとなるmain.dart
のコードは以下の通りです。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: DriftSample(),
);
}
}
class DriftSample extends StatelessWidget {
const DriftSample({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Container()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('Add'),
onPressed: () async {},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('remove'),
onPressed: () async {},
),
),
),
],
),
],
),
),
);
}
}
このコードをベースにアプリを作成していきます。
パッケージのインストール
今回のアプリでは、6つのパッケージを使用します。
以下のようにpubspec.yaml
にパッケージを追加してください。
dependencies:
drift: ^1.5.0
path: ^1.8.0
path_provider: ^2.0.9
sqlite3_flutter_libs: ^0.5.5
dev_dependencies:
build_runner: ^2.1.8
drift_dev: ^1.5.2
コードの自動生成
今回のデータベースはシンプルにIDと内容のみをもったデータベースとします。
このデータベースを構築するために、2段階の方法を行います。
- ベースとなるコードの作成
- コードの自動生成
ベースとなるコードの生成
以下のようにフォルダを構成し、todos.dart
ファイルを作成しましょう。
このtodos.dart
ファイルにデータベースの構成や処理等を記載していきます。
以下のようにコードを追加してください。
import 'package:drift/drift.dart';
//1
part 'todos.g.dart';
//2
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text()();
}
//3
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {}
//1
‘ファイル名.g.dart’の形で記載します。
今回の場合ファイル名が’todos.dart’のため、’todos.g.dart’となります。
この部分はエラーとなりますが、ファイルの自動生成によりエラーは消えるため、
無視して構いません。
//2
データベースのテーブルを定義します。
今回の場合、Todos
というクラス名のため、todos
というテーブルが作成され、
列としてid
とcontent
を持っている、という形になります。
また一行一行が、Todo
というデータクラスで保持されます。
(クラス名からs
を取った形でデータクラスが自動生成されます)
IntColumn
でint
の値を、TextColumn
でString
の値を保持する列を生成します。autoIncrement()
を設定しておくと、データ追加時にid
を自動で生成してくれます。
//3
データベースクラスの定義です。
ここに、データベースの生成処理やデータの追加等の処理を後ほど記載していきます。
@DriftDatabase(tables: [テーブルクラス名])
とアノテーションを追加することで、データベースにテーブルが紐付けられます。
コードの自動生成
ターミナルにて、プロジェクトのルートディレクトリで以下のコードを実行しましょう。
flutter pub run build_runner build
todos.g.dart
というファイルが自動生成されます。
以上でデータベースの構築は完了となります。
データベース生成
ここからは、作成したデータベースとアプリケーションのつなぎ込みを行っていきます。
まず、アプリを立ち上げたときにデータベースを生成する処理を追加します。
todos.dartを以下のように書き換えてください。
import 'dart:io'; //追加
import 'package:drift/drift.dart';
import 'package:drift/native.dart'; //追加
import 'package:path/path.dart' as p; //追加
import 'package:path_provider/path_provider.dart'; //追加
part 'todos.g.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text()();
}
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
//4
MyDatabase() : super(_openConnection()); //追加
//5
@override //追加
int get schemaVersion => 1; //追加
}
//6
//以下追加
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
//4
データベースのインスタンス生成と同時にデータベースとの接続処理を行います。_openConnection
は6にて記述します。
//5
データベースのバージョン指定部分です。
//6
このメソッドでデータベースの保存位置を取得し設定することを行っています。
データベースの生成は、main.dart
のmain
関数の中で、runApp
の前で行います。
import 'package:drift_sample2/src/drift/todos.dart'; //追加
import 'package:flutter/material.dart';
void main() {
final database = MyDatabase(); //追加
runApp(const MyApp());
}
//以下省略
以上がデータベースの生成処理となります。
データの表示
データベース内のデータを表示する方法を記載します。
データベースのデータの取得は以下の2種類があります。
Stream
でデータを監視するFuture
でデータを一気に取得する
todos.dart
のMyDatabase
クラス内に上記2種類の方法を記載します。
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
MyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
//7
//以下追記
Stream<List<Todo>> watchEntries() {
return (select(todos)).watch();
}
//8
//以下追記
Future<List<Todo>> get allTodoEntries => select(todos).get();
}
//7Stream
でのデータ取得のコードです。select
でテーブルを選択し、watch
でデータクラスであるTodo
のリストをStream
で返します。
//8
Futureでのデータ取得のコードです。
上と同様、select
でテーブルを選択し、get
でデータを取得します。
今回はデータの追加等を監視し続けたいため、Stream
のメソッドを用いて、StreamBuilder
にてデータの反映を行います。
以下のようにmain.dart
を書き換えてください。
import 'src/drift/todos.dart';
import 'package:flutter/material.dart';
void main() {
final database = MyDatabase();
//9
runApp(MyApp(database: database)); //変更
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.database, //追加
}) : super(key: key);
final MyDatabase database; //追加
@override
Widget build(BuildContext context) {
return MaterialApp(
//9
home: DriftSample(database: database),//変更
);
}
}
class DriftSample extends StatelessWidget {
const DriftSample({
Key? key,
required this.database, //追加
}) : super(key: key);
final MyDatabase database; //追加
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
//10
//以下、Container()をStreamBuilder(...)に置き換え
child: StreamBuilder(
stream: database.watchEntries(),
builder:
(BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
//11
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => TextButton(
child: Text(snapshot.data![index].content),
onPressed: () async {
},
),
);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('Add'),
onPressed: () async {
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('remove'),
onPressed: () async {
},
),
),
),
],
),
],
),
),
);
}
}
//9
データベースインスタンスを親から子に受け渡します。
//10StreamBuilder
の記述部分です。database.watchEntries
メソッドによりデータの取得を行います。
//11snapshot.data
にはList<Todo>
の型のデータが入っているので、
これを使ってデータを表示します。
以上がデータの表示処理となります。
(まだデータベースにデータが無いため、何もひょうじされません。)
データの追加
データの追加処理を記述していきます。
todos.dart
のMyDatabase
クラス内にデータ追加のメソッドを記載します。
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
MyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Stream<List<Todo>> watchEntries() {
return (select(todos)).watch();
}
Future<List<Todo>> get allTodoEntries => select(todos).get();
//12
//以下追加
Future<int> addTodo(String content) {
return into(todos).insert(TodosCompanion(content: Value(content)));
}
}
//12
データの追加メソッドです。into
でデータを追加するテーブルを指定し、insert
でデータクラスであるTodosCompanion
を追加します。TodosCompanion
はデータの挿入や更新に有用なデータクラスです。
このデータクラスを使うことにより、
idを指定せずにデータを追加したい時など、一部のデータだけ追加することができます。
このメソッドをmain.dart
のDriftSample
クラスのbuild
メソッド内に追加します。
追加したコードは以下のようになります。
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: StreamBuilder(
stream: database.watchEntries(),
builder:
(BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => TextButton(
child: Text(snapshot.data![index].content),
onPressed: () async {},
),
);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('Add'),
onPressed: () async {
//以下追加
await database.addTodo(
'test test test',
);
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('remove'),
onPressed: () async {},
),
),
),
],
),
],
),
),
);
}
以上がデータの追加方法となります。
データの更新
データの更新処理を記述していきます。
todos.dart
のMyDatabase
クラス内にデータ更新のメソッドを記載します。
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
MyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Stream<List<Todo>> watchEntries() {
return (select(todos)).watch();
}
Future<List<Todo>> get allTodoEntries => select(todos).get();
Future<int> addTodo(String content) {
return into(todos).insert(TodosCompanion(content: Value(content)));
}
//13
//以下追加
Future<int> updateTodo(Todo todo, String content) {
return (update(todos)..where((tbl) => tbl.id.equals(todo.id))).write(
TodosCompanion(
content: Value(content),
),
);
}
}
//13
データ更新のメソッドです。update(todos)
でテーブルを指定し、where
以下で引数のTodo
インスタンスとid
が一致する物を探します。
探索で見つかった行を、write
で上書きする、といった流れのメソッドとなります。
このメソッドをmain.dart
のDriftSample
クラスのbuild
メソッド内に追加します。
追加したコードは以下のようになります。
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: StreamBuilder(
stream: database.watchEntries(),
builder:
(BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => TextButton(
child: Text(snapshot.data![index].content),
onPressed: () async {
//以下追加
await database.updateTodo(
snapshot.data![index],
'updated',
);
},
),
);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('Add'),
onPressed: () async {
await database.addTodo(
'test test test',
);
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('remove'),
onPressed: () async {},
),
),
),
],
),
],
),
),
);
}
以上がデータの更新方法となります。
データの削除
データの削除処理を記述していきます。
todos.dart
のMyDatabase
クラス内にデータ削除のメソッドを記載します。
@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
MyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Stream<List<Todo>> watchEntries() {
return (select(todos)).watch();
}
Future<List<Todo>> get allTodoEntries => select(todos).get();
Future<int> addTodo(String content) {
return into(todos).insert(TodosCompanion(content: Value(content)));
}
Future<int> updateTodo(Todo todo, String content) {
return (update(todos)..where((tbl) => tbl.id.equals(todo.id))).write(
TodosCompanion(
content: Value(content),
),
);
}
//14
//以下追加
Future<void> deleteTodo(Todo todo) {
return (delete(todos)..where((tbl) => tbl.id.equals(todo.id))).go();
}
}
//13
データ削除のメソッドです。delete(todos)
でテーブルを指定し、where
以下で引数のTodo
インスタンスとid
が一致する物を探します。
探索で見つかった行を、go で削除実行する、といった流れのメソッドとなります。
このメソッドをmain.dart
のDriftSample
クラスのbuild
メソッド内に追加します。
追加したコードは以下のようになります。
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: StreamBuilder(
stream: database.watchEntries(),
builder:
(BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => TextButton(
child: Text(snapshot.data![index].content),
onPressed: () async {
await database.updateTodo(
snapshot.data![index],
'updated',
);
},
),
);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('Add'),
onPressed: () async {
await database.addTodo(
'test test test',
);
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
child: const Text('remove'),
onPressed: () async {
//15
//以下追加
final list = await database.allTodoEntries;
if (list.isNotEmpty) {
await database.deleteTodo(list[list.length - 1]);
}
},
),
),
),
],
),
],
),
),
);
}
//15await database.allTodoEntries
で データベース内のデータを全件取得します。
その後、一番最後のデータを削除する、といった流れで削除処理を行っています。
以上で、データベースの作成、データの表示、追加、更新、削除をするアプリができました。
今回のアプリの全体のコードは以下のリポジトリにあります。
併せてご確認ください。
まとめ
Flutterで内部データベースを簡単に扱えるようにするライブラリとして、
Driftを紹介しました。
基本的な使用方法が理解できるように、
内部データベースからのデータの表示、追加、更新、削除のできる
サンプルコードで解説しました。
Driftのライブラリでは、他にもいろいろな設定があります。
こちらの公式ドキュメントにて解説されているため、
興味がある方、ぜひこちらも併せて読んでみてください。
本記事があなたのアプリ開発の一助となれば幸いです。
編集後記(2022/4/13)
Flutterで内部データベースを扱いやすくするパッケージとして、
『Moor』を思い浮かべる人がいらっしゃるかもしれません。
この『Moor』と『Drift』は兄弟のような関係にあります。
『Moor』の名前を変えたものが『Drift』という位置づけです。
なぜ名前を変える必要があったのでしょうか。
もともと、Androidの内部データベースを使いやすくするライブラリとして
『Room』があり、それを逆から読んで『Moor』としたそうです。
こちらによると、『Moor』という言葉は、一部の地域であまり良くない表現だったため、
名前を変える判断に至ったそうです。
造語のようなものを作るときは、それが良くない表現かどうか、
しっかりと確認する必要がある、ということがよく分かるエピソードですよね。
アプリの名前をつけるときなど、特に気をつけたいものです。
週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。