【Flutter】 Drift の基本的な使い方解説

Flutterで内部データベースを扱うのに、良い方法ないかな?

Dartだけで、簡単に内部データベースを使いたいわ!

本記事では、そんな質問、要望にお答えします。

Flutterで内部データベースを簡単に扱えるようにするライブラリとして、
Driftを紹介します。

基本的な使用方法が理解できるように、
内部データベースからのデータの表示、追加、更新、削除のできる
サンプルコードで解説します。

内部データベースの選定に悩んでいる人には有用かと思います。
ぜひ読んでみてください!

Drift とは

「アプリを落としたらデータが消えてしまった!」

あなたはこのような経験はないでしょうか。

データの永続化を行っていないアプリを落とすと、
それまで記録されていたデータは削除されてしまいます。

Drift とは Dart/Flutterのアプリケーションでデータの永続化を行うためのライブラリです。
sqflitesql.js のようなデータベースライブラリの上に構築されていて、
Streamでデータを受け取ったり、
SQLの知識が無くともDartのみでデータの追加や更新ができる機能を持っています。

データベースとは、以下の例のように整理された情報の集まりです。

Firebaseのように外部サーバーに用意されたデータベースを外部データベースと呼ぶのに対し、
スマートフォンなどの記憶領域に用意するデータベースを内部データベースと呼びます。
Driftは、内部データベースを扱いやすくするライブラリとなります。

特にDriftは、以下の時に有用なライブラリとなっています。

  • データベース形式で内部にデータを保存したい
  • StringBoolInt などの基本的な型しか取り扱わない

基本的な使い方

簡単なアプリの作成を通し、基本的な使い方を解説していきます。

上の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

各パッケージのバージョンは、以下のリンクから確認ください。

drift
path
path_provider
sqlite3_flutter_libs
build_runner
drift_dev

2022年4月時点で、
pathのバージョンとflutter_testのバージョンの競合で
flutter pub get ができなくなる事象があります。

そのため、pathパッケージのバージョンをあえて1.8.0に設定しています。

詳細はこちらの記事をご確認ください。

[Flutter]2022年3月時点ではDriftパッケージでpath1.8.1を使おうとするとエラーになります(flutter_test from sdk is forbidden)
Flutterで、SQL文を使わずにDartだけで「SQLite」のデータベースが操作できる「Drift(旧:…

コードの自動生成

今回のデータベースはシンプルにIDと内容のみをもったデータベースとします。

このデータベースを構築するために、2段階の方法を行います。

  1. ベースとなるコードの作成
  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というテーブルが作成され、
列としてidcontentを持っている、という形になります。
また一行一行が、Todoというデータクラスで保持されます。
(クラス名からsを取った形でデータクラスが自動生成されます)

IntColumnintの値を、TextColumnStringの値を保持する列を生成します。
autoIncrement()を設定しておくと、データ追加時にidを自動で生成してくれます。

//3
データベースクラスの定義です。
ここに、データベースの生成処理やデータの追加等の処理を後ほど記載していきます。

@DriftDatabase(tables: [テーブルクラス名])
とアノテーションを追加することで、データベースにテーブルが紐付けられます。

コードの自動生成

ターミナルにて、プロジェクトのルートディレクトリで以下のコードを実行しましょう。

flutter pub run build_runner build

todos.g.dartというファイルが自動生成されます。

もし、コードに変更を加えて再生成したい場合は、

flutter pub run build_runner build --delete-conflicting-outputs

を実行しましょう。

自動生成ファイルを静的解析の対象から外すため、
analysis_options.dartに以下のコードを追加することをオススメします。

analyzer:
  exclude: 
    - lib/**/**/*.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.dartmain関数の中で、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.dartMyDatabaseクラス内に上記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();
}

//7
Streamでのデータ取得のコードです。
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
データベースインスタンスを親から子に受け渡します。

今回は単純なアプリのため、親から子へのインスタンス受け渡しで記述しています。
構造が複雑になる場合は、ProviderRiverpodなどの状態管理手法を利用してください。

//10
StreamBuilderの記述部分です。
database.watchEntriesメソッドによりデータの取得を行います。

//11
snapshot.dataにはList<Todo>の型のデータが入っているので、
これを使ってデータを表示します。

以上がデータの表示処理となります。

(まだデータベースにデータが無いため、何もひょうじされません。)

データの追加

データの追加処理を記述していきます。

todos.dartMyDatabaseクラス内にデータ追加のメソッドを記載します。

@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.dartDriftSampleクラスの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.dartMyDatabaseクラス内にデータ更新のメソッドを記載します。

@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.dartDriftSampleクラスの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.dartMyDatabaseクラス内にデータ削除のメソッドを記載します。

@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.dartDriftSampleクラスの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]);
                        }
                      },
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

//15
await database.allTodoEntriesで データベース内のデータを全件取得します。
その後、一番最後のデータを削除する、といった流れで削除処理を行っています。

以上で、データベースの作成、データの表示、追加、更新、削除をするアプリができました。

今回のアプリの全体のコードは以下のリポジトリにあります。
併せてご確認ください。

GitHub - Umigishi-Aoi/drift_sample: This is Flutter Project. This Project is simple sample of drift.
This is Flutter Project. This Project is simple sample of drift. - GitHub - Umigishi-Aoi/drift_sample: This is Flutter Project. This Project is simple sample of...

まとめ

Flutterで内部データベースを簡単に扱えるようにするライブラリとして、
Driftを紹介しました。

基本的な使用方法が理解できるように、
内部データベースからのデータの表示、追加、更新、削除のできる
サンプルコードで解説しました。

Driftのライブラリでは、他にもいろいろな設定があります。

こちらの公式ドキュメントにて解説されているため、
興味がある方、ぜひこちらも併せて読んでみてください。

Drift

本記事があなたのアプリ開発の一助となれば幸いです。

Flutterを一緒に学んでみませんか?
Flutter エンジニアに特化した学習コミュニティ、Flutter大学への入会は、
以下の画像リンクから。



編集後記(2022/4/13)

Flutterで内部データベースを扱いやすくするパッケージとして、
『Moor』を思い浮かべる人がいらっしゃるかもしれません。

この『Moor』と『Drift』は兄弟のような関係にあります。
『Moor』の名前を変えたものが『Drift』という位置づけです。

なぜ名前を変える必要があったのでしょうか。

もともと、Androidの内部データベースを使いやすくするライブラリとして
『Room』があり、それを逆から読んで『Moor』としたそうです。
こちらによると、『Moor』という言葉は、一部の地域であまり良くない表現だったため、
名前を変える判断に至ったそうです。

造語のようなものを作るときは、それが良くない表現かどうか、
しっかりと確認する必要がある、ということがよく分かるエピソードですよね。

アプリの名前をつけるときなど、特に気をつけたいものです。

週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。

タイトルとURLをコピーしました