転職したらスマレジだった件

スマレジのエンジニアやまてのテックブログです。マジレス大歓迎です。

Flutter|Drift パッケージでデータ保存を永続化する方法

スマレジの テックファーム(SES 部門) でWebエンジニアとして働いている やまて(@r_yamate) と申します。

実務では、2023 年 3 月末で SES の派遣先で、テーブルオーダーシステムの機能改修業務の設計などを担当していた業務を終えたところです。

4月からは、スマレジの関連アプリの開発業務を担当しています。触ったことのなかった Flutter での開発で、日々奮闘中です。

はじめに

本記事では、 Flutter の Drift という SQLite パッケージを使って、データを永続化する方法について書きます。 Drift を使うと、 SQLite データベースの操作を簡単に、安全に行うことができます。

CRUD 操作(作成、参照、更新、削除)を実装して、メモアプリを作成することを通して、 Drift パッケージの使い方を確認します。

Flutter パッケージ「Drift」

Drift は、 Flutter と Dart のためのリアクティブな永続化ライブラリで、 SQLite の上に構築されており、 Dart でデータベースを操作するための機能を提供するパッケージです。

pub.dev

何が良いかというと、ローカル(スマホの端末)の保存領域にデータを保持できて、データの追加や削除、更新が行える点です。

目次

メモアプリの概要

まず、作成するメモアプリの設計を簡単に確認します。

  • このアプリは以下のようなCRUD 操作の機能を持つことを想定しています。
    • メモの一覧表示
    • メモの作成
    • メモの編集
    • メモの削除
  • 各メモの項目は、「タイトル」と「内容」です。

完成イメージ

環境

GitHub リポジトリ

本記事で使用するサンプルアプリのソースコードは、以下の GitHub リポジトリに公開しています。

github.com

1. Drift のセットアップ

新しい Flutter プロジェクトを作成し、 Drift パッケージをセットアップします。

1-1. Flutter プロジェクトの作成

以下のコマンドを使って新しい Flutter プロジェクトを作成します。

flutter create memo_app
  • 実行時ログ

      # r_yamate @ mbp in ~/development [0:21:07] 
      $ flutter create flutter_memo_app
      Creating project flutter_memo_app...
      Running "flutter pub get" in flutter_memo_app...
      Resolving dependencies in flutter_memo_app... (2.6s)
      + async 2.10.0 (2.11.0 available)
      + boolean_selector 2.1.1
      + characters 1.2.1 (1.3.0 available)
      + clock 1.1.1
      + collection 1.17.0 (1.17.2 available)
      + cupertino_icons 1.0.5
      + fake_async 1.3.1
      + flutter 0.0.0 from sdk flutter
      + flutter_lints 2.0.1
      + flutter_test 0.0.0 from sdk flutter
      + js 0.6.5 (0.6.7 available)
      + lints 2.0.1 (2.1.1 available)
      + matcher 0.12.13 (0.12.16 available)
      + material_color_utilities 0.2.0 (0.5.0 available)
      + meta 1.8.0 (1.9.1 available)
      + path 1.8.2 (1.8.3 available)
      + sky_engine 0.0.99 from sdk flutter
      + source_span 1.9.1 (1.10.0 available)
      + stack_trace 1.11.0
      + stream_channel 2.1.1
      + string_scanner 1.2.0
      + term_glyph 1.2.1
      + test_api 0.4.16 (0.6.0 available)
      + vector_math 2.1.4
      Changed 24 dependencies in flutter_memo_app!
      Wrote 127 files.
    
      All done!
      You can find general documentation for Flutter at: https://docs.flutter.dev/
      Detailed API documentation is available at: https://api.flutter.dev/
      If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev
    
      In order to run your application, type:
    
        $ cd flutter_memo_app
        $ flutter run
    
      Your application code is in flutter_memo_app/lib/main.dart.
    
      # r_yamate @ srC02T54QWH03M in ~/development [0:34:22] 
      $ cd flutter_memo_app
    

1-2. Drift および関連パッケージの追加

次に、 pubspec.yaml ファイルを開き、次の依存関係を追加します。

dependencies:
  flutter:
    sdk: flutter
+  drift: ^2.4.2
+  sqlite3_flutter_libs: ^0.5.0
+  path_provider: ^2.0.0
+  path: ^1.8.2

  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
+  build_runner: ^2.3.3
+  drift_dev: ^2.4.1

追加したパッケージ

そして、以下のコマンドでパッケージを取得します。

flutter pub get
  • 実行時ログ

      # r_yamate @ mbp in ~/development/flutter_memo_app on git:main x [1:42:52] 
      $ flutter pub get
      Running "flutter pub get" in flutter_memo_app...
      Resolving dependencies... (6.3s)
        _fe_analyzer_shared 50.0.0 (61.0.0 available)
        analyzer 5.2.0 (5.13.0 available)
        args 2.4.1 (2.4.2 available)
      > async 2.10.0 (was 2.9.0) (2.11.0 available)
      > boolean_selector 2.1.1 (was 2.1.0)
        build 2.3.1 (2.4.0 available)
        build_daemon 3.1.1 (4.0.0 available)
        build_resolvers 2.1.0 (2.2.0 available)
        build_runner 2.3.3 (2.4.5 available)
        build_runner_core 7.2.7 (7.2.10 available)
        characters 1.2.1 (1.3.0 available)
        checked_yaml 2.0.2 (2.0.3 available)
        cli_util 0.3.5 (0.4.0 available)
        code_builder 4.4.0 (4.5.0 available)
      > collection 1.17.0 (was 1.16.0) (1.17.2 available)
        crypto 3.0.2 (3.0.3 available)
        dart_style 2.2.5 (2.3.1 available)
        drift 2.4.2 (2.8.2 available)
        drift_dev 2.4.1 (2.8.3 available)
        file 6.1.4 (7.0.0 available)
        fixnum 1.0.1 (1.1.0 available)
        glob 2.1.1 (2.1.2 available)
        js 0.6.5 (0.6.7 available)
        json_annotation 4.8.0 (4.8.1 available)
        lints 2.0.1 (2.1.1 available)
        logging 1.1.1 (1.2.0 available)
      > matcher 0.12.13 (was 0.12.12) (0.12.16 available)
      > material_color_utilities 0.2.0 (was 0.1.5) (0.5.0 available)
        meta 1.8.0 (1.9.1 available)
        path 1.8.2 (1.8.3 available)
      > source_span 1.9.1 (was 1.9.0) (1.10.0 available)
        sqlparser 0.26.1 (0.30.2 available)
      > stack_trace 1.11.0 (was 1.10.0)
      > stream_channel 2.1.1 (was 2.1.0)
      > string_scanner 1.2.0 (was 1.1.1)
      > test_api 0.4.16 (was 0.4.12) (0.6.0 available)
      > vector_math 2.1.4 (was 2.1.2)
        watcher 1.0.2 (1.1.0 available)
        win32 4.1.4 (5.0.3 available)
        yaml 3.1.1 (3.1.2 available)
      Changed 11 dependencies!
    

2. データベースの生成

データベースファイルを作成して、アプリを起動したときにデータベースを生成する処理を追加します。

2-1. データベースファイルの作成

lib ディレクトリに database ディレクトリを作成し、memos.dart を作成します。そして、以下のように Memo テーブルと、 AppDatabase データベースを定義します。

  • 新規作成:lib/database/memos.dart
import 'package:drift/drift.dart';

part 'memos.g.dart';

@DataClassName('Memo')
class Memos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 50)();
  TextColumn get content => text().withLength(min: 1, max: 1000)();
}

@DriftDatabase(tables: [Memos])
class AppDatabase extends _$AppDatabase {}

Drift では、テーブルの定義は特殊なクラスを使って行われます。このクラスはテーブルの各行を表すものです。メモアプリを作成するため、 Memo という名前のテーブルを定義します。

メモのモデルクラスの定義

/// `Memo`という名前で、メモ情報を保持するクラス。
@DataClassName('Memo')
class Memos extends Table {
  /// メモのID。自動インクリメントする整数値。
  IntColumn get id => integer().autoIncrement()();

  /// メモのタイトル。1から50文字のテキスト。
  TextColumn get title => text().withLength(min: 1, max: 50)();

  /// メモの内容。1から1000文字のテキスト。
  TextColumn get content => text().withLength(min: 1, max: 1000)();
}

Memos というテーブルを定義し、3つのフィールド(id, title, content)を持たせます。

  • id:自動インクリメントする整数型
  • titlecontent:テキスト型で、それぞれ最小値と最大値の長さを設定

また、 @DataClassName('Memo') というアノテーションを使って、このテーブルの各行の型の名前を Memo に設定します。これにより、このテーブルの各行は Memo という型を持つことになります。

データベースクラスの定義

/// `Memos`テーブルを含むデータベースを表現するクラス。
///
/// データベースの構造や動作をコードによってモデリングする。
@DriftDatabase(tables: [Memos])
class AppDatabase extends _$AppDatabase {}

このコードは、データベースクラスの定義です。ここに、データベースの生成処理やデータの追加等の処理を記載します。

2-2. ローカルデータベースの自動生成

以下のコマンドを実行します。

flutter pub run build_runner build
  • 実行時ログ

      # r_yamate @ mbp in ~/development/flutter_memo_app on git:main x [16:09:24] 
      $ flutter pub run build_runner build
      [INFO] Generating build script...
      [INFO] Generating build script completed, took 587ms
    
      [INFO] Initializing inputs
      [INFO] Reading cached asset graph...
      [INFO] Reading cached asset graph completed, took 99ms
    
      [INFO] Checking for updates since last build...
      [INFO] Checking for updates since last build completed, took 903ms
    
      [INFO] Running build...
      [INFO] Running build completed, took 32ms
    
      [INFO] Caching finalized dependency graph...
      [INFO] Caching finalized dependency graph completed, took 83ms
    
      [INFO] Succeeded after 133ms with 0 outputs (0 actions)
    

こちらは、 build_runner パッケージの機能で、コマンドを実行することで、drift パッケージを使って Flutter アプリで使用するためのローカルデータベースの構造や設計が自動生成されます。

build_runner | Dart Package

自動生成されたファイル

以下のファイルが自動生成されました。自動生成されたコードは一般的には、開発者が直接編集することはないようです。代わりに、 Drift パッケージが提供するアノテーションとツールを使用して、データベーススキーマを定義し、そのスキーマに基づいてこのようなコードを自動生成します。

lib/database/memos.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'memos.dart';

// ignore_for_file: type=lint
class $MemosTable extends Memos with TableInfo<$MemosTable, Memo> {
  @override
  final GeneratedDatabase attachedDatabase;
  final String? _alias;
  $MemosTable(this.attachedDatabase, [this._alias]);
  static const VerificationMeta _idMeta = const VerificationMeta('id');
  @override
  late final GeneratedColumn<int> id = GeneratedColumn<int>(
      'id', aliasedName, false,
      hasAutoIncrement: true,
      type: DriftSqlType.int,
      requiredDuringInsert: false,
      defaultConstraints:
          GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
  static const VerificationMeta _titleMeta = const VerificationMeta('title');
  @override
  late final GeneratedColumn<String> title = GeneratedColumn<String>(
      'title', aliasedName, false,
      additionalChecks:
          GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50),
      type: DriftSqlType.string,
      requiredDuringInsert: true);
  static const VerificationMeta _contentMeta =
      const VerificationMeta('content');
  @override
  late final GeneratedColumn<String> content = GeneratedColumn<String>(
      'content', aliasedName, false,
      additionalChecks: GeneratedColumn.checkTextLength(
          minTextLength: 1, maxTextLength: 1000),
      type: DriftSqlType.string,
      requiredDuringInsert: true);
  @override
  List<GeneratedColumn> get $columns => [id, title, content];
  @override
  String get aliasedName => _alias ?? 'memos';
  @override
  String get actualTableName => 'memos';
  @override
  VerificationContext validateIntegrity(Insertable<Memo> instance,
      {bool isInserting = false}) {
    final context = VerificationContext();
    final data = instance.toColumns(true);
    if (data.containsKey('id')) {
      context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
    }
    if (data.containsKey('title')) {
      context.handle(
          _titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
    } else if (isInserting) {
      context.missing(_titleMeta);
    }
    if (data.containsKey('content')) {
      context.handle(_contentMeta,
          content.isAcceptableOrUnknown(data['content']!, _contentMeta));
    } else if (isInserting) {
      context.missing(_contentMeta);
    }
    return context;
  }

  @override
  Set<GeneratedColumn> get $primaryKey => {id};
  @override
  Memo map(Map<String, dynamic> data, {String? tablePrefix}) {
    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
    return Memo(
      id: attachedDatabase.typeMapping
          .read(DriftSqlType.int, data['${effectivePrefix}id'])!,
      title: attachedDatabase.typeMapping
          .read(DriftSqlType.string, data['${effectivePrefix}title'])!,
      content: attachedDatabase.typeMapping
          .read(DriftSqlType.string, data['${effectivePrefix}content'])!,
    );
  }

  @override
  $MemosTable createAlias(String alias) {
    return $MemosTable(attachedDatabase, alias);
  }
}

class Memo extends DataClass implements Insertable<Memo> {
  final int id;
  final String title;
  final String content;
  const Memo({required this.id, required this.title, required this.content});
  @override
  Map<String, Expression> toColumns(bool nullToAbsent) {
    final map = <String, Expression>{};
    map['id'] = Variable<int>(id);
    map['title'] = Variable<String>(title);
    map['content'] = Variable<String>(content);
    return map;
  }

  MemosCompanion toCompanion(bool nullToAbsent) {
    return MemosCompanion(
      id: Value(id),
      title: Value(title),
      content: Value(content),
    );
  }

  factory Memo.fromJson(Map<String, dynamic> json,
      {ValueSerializer? serializer}) {
    serializer ??= driftRuntimeOptions.defaultSerializer;
    return Memo(
      id: serializer.fromJson<int>(json['id']),
      title: serializer.fromJson<String>(json['title']),
      content: serializer.fromJson<String>(json['content']),
    );
  }
  @override
  Map<String, dynamic> toJson({ValueSerializer? serializer}) {
    serializer ??= driftRuntimeOptions.defaultSerializer;
    return <String, dynamic>{
      'id': serializer.toJson<int>(id),
      'title': serializer.toJson<String>(title),
      'content': serializer.toJson<String>(content),
    };
  }

  Memo copyWith({int? id, String? title, String? content}) => Memo(
        id: id ?? this.id,
        title: title ?? this.title,
        content: content ?? this.content,
      );
  @override
  String toString() {
    return (StringBuffer('Memo(')
          ..write('id: $id, ')
          ..write('title: $title, ')
          ..write('content: $content')
          ..write(')'))
        .toString();
  }

  @override
  int get hashCode => Object.hash(id, title, content);
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is Memo &&
          other.id == this.id &&
          other.title == this.title &&
          other.content == this.content);
}

class MemosCompanion extends UpdateCompanion<Memo> {
  final Value<int> id;
  final Value<String> title;
  final Value<String> content;
  const MemosCompanion({
    this.id = const Value.absent(),
    this.title = const Value.absent(),
    this.content = const Value.absent(),
  });
  MemosCompanion.insert({
    this.id = const Value.absent(),
    required String title,
    required String content,
  })  : title = Value(title),
        content = Value(content);
  static Insertable<Memo> custom({
    Expression<int>? id,
    Expression<String>? title,
    Expression<String>? content,
  }) {
    return RawValuesInsertable({
      if (id != null) 'id': id,
      if (title != null) 'title': title,
      if (content != null) 'content': content,
    });
  }

  MemosCompanion copyWith(
      {Value<int>? id, Value<String>? title, Value<String>? content}) {
    return MemosCompanion(
      id: id ?? this.id,
      title: title ?? this.title,
      content: content ?? this.content,
    );
  }

  @override
  Map<String, Expression> toColumns(bool nullToAbsent) {
    final map = <String, Expression>{};
    if (id.present) {
      map['id'] = Variable<int>(id.value);
    }
    if (title.present) {
      map['title'] = Variable<String>(title.value);
    }
    if (content.present) {
      map['content'] = Variable<String>(content.value);
    }
    return map;
  }

  @override
  String toString() {
    return (StringBuffer('MemosCompanion(')
          ..write('id: $id, ')
          ..write('title: $title, ')
          ..write('content: $content')
          ..write(')'))
        .toString();
  }
}

abstract class _$AppDatabase extends GeneratedDatabase {
  _$AppDatabase(QueryExecutor e) : super(e);
  late final $MemosTable memos = $MemosTable(this);
  @override
  Iterable<TableInfo<Table, Object?>> get allTables =>
      allSchemaEntities.whereType<TableInfo<Table, Object?>>();
  @override
  List<DatabaseSchemaEntity> get allSchemaEntities => [memos];
}
  • $MemosTableクラス:「memos」という名前のテーブルについて定義しています。このテーブルには、idtitlecontentの3つのカラムがあります。それぞれのカラムは GeneratedColumn として、各カラムの型や他の制約を定義しています。
  • Memoクラス: memos テーブルのレコードを定義するためのデータクラスです。各メモは、整数型の id 、文字列型のtitle 、文字列型の content を持っています。
  • MemosCompanionクラス:新しいメモをデータベースに挿入する際のヘルパークラスです。任意のカラムを含むレコードを作成することができます。
  • _$AppDatabaseクラス:全てのテーブルとその他のデータベーススキーマエンティティを保持します。今回は、 memos テーブルのみが存在します。

補足. ファイルを再生成したい場合

ファイルを再生成したい場合は、--delete-conflicting-outputs というオプションをコマンドに付けて実行します。

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

build_runner | Dart Package

2-3. データベースの生成

アプリを起動したときにデータベースを生成する処理を追加します。

編集:lib/database/memos.dart

import 'dart:io';

import 'package:drift/drift.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

part 'memos.g.dart';

@DataClassName('Memo')
class Memos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 50)();
  TextColumn get content => text().withLength(min: 1, max: 1000)();
}

/// `Memos`テーブルを含むデータベースを表現するクラス。
@DriftDatabase(tables: [Memos])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

    /// データベースのスキーマバージョンを返す。現在は1。
  @override
  int get schemaVersion => 1;
}

/// アプリのドキュメントディレクトリにデータベースファイルを生成し、接続する。
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'memos.sqlite'));
    return NativeDatabase(file);
  });
}

データベースの生成は、main.dartmain関数の中で、runAppの前で行います。

編集:lib/main.dart

import 'package:flutter/material.dart';

import 'database/memos.dart';

void main() {
+  final database = AppDatabase();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

3. メモの CRUD 操作の作成

以下の CRUD の操作を作成します。

  • メモの一覧表示
  • メモの作成
  • メモの編集
  • メモの削除

3-1. データベースクラスの編集

メモの作成、読み取り、更新、削除(CRUD)の操作は、 memos.dart ファイルの AppDatabase クラスにメソッドとして追加します。

編集:lib/database/memos.dart

import 'dart:io';

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

part 'memos.g.dart';

@DataClassName('Memo')
class Memos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 50)();
  TextColumn get content => text().withLength(min: 1, max: 1000)();
}

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

  @override
  int get schemaVersion => 1;
}

/// データベースから全てのメモをストリームとして取得する。
/// メモが追加、更新、削除されると、このストリームは新しいリストを返す。
Stream<List<Memo>> watchAllMemos(AppDatabase db) {
  return db.select(db.memos).watch();
}

/// データベースから全てのメモを一度だけ取得する。
Future<List<Memo>> getAllMemos(AppDatabase db) {
  return db.select(db.memos).get();
}

/// 新しいメモをデータベースに挿入する。
Future insertMemo(AppDatabase db, Memo memo) {
  return db.into(db.memos).insert(memo);
}

/// メモを更新する。
Future updateMemo(AppDatabase db, Memo memo) {
  return db.update(db.memos).replace(memo);
}

/// データベースからメモを削除する。
Future deleteMemo(AppDatabase db, Memo memo) {
  return db.delete(db.memos).delete(memo);
}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'memos.sqlite'));
    return NativeDatabase(file);
  });
}

これで、アプリのバックエンド部分であるデータベースとデータ操作が完成しました。

3-2. フロントエンドの UI の作成

フロントエンドのUI部分を作成します。

編集:lib/main.dart

import 'package:flutter/material.dart';

import 'database/memos.dart';

void main() {
  final database = AppDatabase();
  runApp(MyApp(database: database));
}

class MyApp extends StatelessWidget {
  /// MyApp のコンストラクタ。
  ///
  /// [database] はメモの保存や取得に使われるデータベースオブジェクト。
  const MyApp({
    Key? key,
    required this.database,
  }) : super(key: key);

  final AppDatabase database;

  /// MyApp のビルドメソッド。MaterialApp ウィジェットを返す。
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Drift Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(
        title: 'Drift Memo App',
        database: database,
      ),
    );
  }
}

/// アプリのホームページとなるウィジェット。
class MyHomePage extends StatefulWidget {
  /// MyHomePage のコンストラクタ。
  ///
  /// [title] はアプリのタイトル。
  /// [database] はメモの保存や取得に使われるデータベースオブジェクト。
  const MyHomePage({
    Key? key,
    required this.title,
    required this.database,
  }) : super(key: key);

  final String title;
  final AppDatabase database;

  /// MyHomePage の State を生成する。
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

/// MyHomePage の状態を管理するクラス。
class _MyHomePageState extends State<MyHomePage> {
  late final TextEditingController _titleController;
  late final TextEditingController _contentController;
  int? _editingMemoId;

  /// State の初期化時に実行される。
  ///
  /// メモのタイトルと内容を編集するための TextEditingController を初期化する。
  @override
  void initState() {
    super.initState();
    _titleController = TextEditingController();
    _contentController = TextEditingController();
  }

  /// Stateの破棄時に実行される。
  ///
  /// メモのタイトルと内容を編集するための TextEditingController を破棄する。
  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  /// 新しいメモを挿入または既存のメモを更新する。
  Future<void> _insertOrUpdateMemo() async {
    final title = _titleController.text;
    final content = _contentController.text;
    if (_editingMemoId != null) {
      final memo = Memo(
        id: _editingMemoId!,
        title: title,
        content: content,
      );
      await updateMemo(widget.database, memo);
    } else {
      final id = DateTime.now().millisecondsSinceEpoch;
      final memo = Memo(
        id: id,
        title: title,
        content: content,
      );
      await insertMemo(widget.database, memo);
    }
    _titleController.clear();
    _contentController.clear();
    _editingMemoId = null;
  }

  /// メモを削除する。
  Future<void> _deleteMemo(Memo memo) async {
    await deleteMemo(widget.database, memo);
  }

  /// メモの編集を開始する。
  void _startEditingMemo(Memo memo) {
    _editingMemoId = memo.id;
    _titleController.text = memo.title;
    _contentController.text = memo.content;
  }

  /// MyHomePage のビルドメソッド。 UI を構築する。
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: true,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: StreamBuilder<List<Memo>>(
        stream: watchAllMemos(widget.database),
        builder: (context, snapshot) {
          final memos = snapshot.data ?? [];
          return ListView.builder(
            itemCount: memos.length,
            itemBuilder: (context, index) {
              final memo = memos[index];
              return ListTile(
                title: Text(memo.title),
                subtitle: Text(memo.content),
                trailing: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    IconButton(
                      icon: const Icon(Icons.edit),
                      onPressed: () => _startEditingMemo(memo),
                    ),
                    IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () => _deleteMemo(memo),
                    ),
                  ],
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _insertOrUpdateMemo,
        tooltip: 'Insert Memo',
        child: const Icon(Icons.add),
      ),
      bottomSheet: SizedBox(
        child: SingleChildScrollView(
          padding: const EdgeInsets.only(
            left: 16,
            top: 32,
            right: 16,
            bottom: 16,
          ),
          child: Column(
            children: [
              TextField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: 'Title',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _contentController,
                decoration: const InputDecoration(
                  labelText: 'Content',
                  border: OutlineInputBorder(),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

説明は割愛しますが、以下の機能を作成しました。

  • メモの一覧表示
  • メモの作成
  • メモの編集
  • メモの削除

上記のとおり UI については微妙な仕上がりで手直ししたい気持ちですが、主目的は Drift パッケージの理解なので、今回は深入りしません。

まとめ

以上で基本的なメモアプリの機能が完成しました。

Flutter の Drift パッケージを使ってデータを永続化する方法について、メモアプリの作成、CRUD 操作(作成、参照、更新、削除)の実装を通して書きました。

ありがとうございました。



今回の記事の内容も然り。