Skip to content
Go back

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

by Aoi
Speaker
Flutterで内部データベースを扱うのに、良い方法ないかな?
Speaker
Dartだけで、簡単に内部データベースを使いたいわ!

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

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

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

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

Drift とは

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

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

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

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

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

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

SQLとは、データベースを扱うためのデータベース言語です。

本記事ではSQLについて理解していなくてもデータベースを扱えるように解説しますが、 詳細について知りたい方は、下記ページが参考になりますので、読んでみてください。

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

基本的な使い方

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

上の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に設定しています。

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

コードの自動生成

今回のデータベースはシンプルに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種類があります。

todos.dartMyDatabaseクラス内に上記2種類の方法を記載します。

@DriftDatabase(tables: [Todos])
class MyDatabase extends _$MyDatabase {
  MyDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;
  //7
  //以下追記
  Stream> watchEntries() {
    return (select(todos)).watch();
  }
  //8
  //以下追記
  Future> 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> 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> watchEntries() {
    return (select(todos)).watch();
  }

  Future> get allTodoEntries => select(todos).get();
  //12
  //以下追加
  Future 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> 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> watchEntries() {
    return (select(todos)).watch();
  }

  Future> get allTodoEntries => select(todos).get();

  Future addTodo(String content) {
    return into(todos).insert(TodosCompanion(content: Value(content)));
  }

//13
//以下追加
  Future 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> 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> watchEntries() {
    return (select(todos)).watch();
  }

  Future> get allTodoEntries => select(todos).get();

  Future addTodo(String content) {
    return into(todos).insert(TodosCompanion(content: Value(content)));
  }

  Future updateTodo(Todo todo, String content) {
    return (update(todos)..where((tbl) => tbl.id.equals(todo.id))).write(
      TodosCompanion(
        content: Value(content),
      ),
    );
  }
//14
//以下追加
  Future 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> 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で データベース内のデータを全件取得します。 その後、一番最後のデータを削除する、といった流れで削除処理を行っています。

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

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

まとめ

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

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

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

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

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

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

編集後記(2022/4/13)

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

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

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

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

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

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

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


Aoiのプロフィール画像

Aoi

ライター兼個人Flutter開発者 Flutterにて5つのアプリを開発。QiitaではFlutter記事にて約500のContributionを獲得。

Share this post on:

Previous Post
【Flutter】線の軌跡をアニメーションしよう!【 path_animator 】
Next Post
Flutter ニュース 【2022年4月第2週】