Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

FlutterでDriftを使ったデータ保存でのつまづきと対策

こんにちは、クライアントグループの樋口です。

今回は、弊社のアプリにてFlutterで使えるローカルDBパッケージの「Drift」を用いた開発した際に生じた、データ保存でのつまづきとその対処法を紹介します。さらに、Driftの簡単な使い方と使用例も併せて紹介します。

環境

  • Flutter 3.13.8
  • drift 2.13.0
  • freezed 2.4.5

Driftについて

DriftとはDartを利用してデータを永続化できるパッケージです。端末内部にデータを永続化し、DartAPIまたはSQLによってデータの操作を行えます。 サーバーを経由しないため、オフラインでも保存し操作できるというのが特徴です。弊社でも、オフラインでデータを保存する際にDriftを用いています。 pub.dev

Driftの簡単な使い方

Driftの導入やインストールに関しては、今回は割愛させていただきます。必要な方は公式ドキュメント を参考にしてみていただけると幸いです。このパートでは、公式ドキュメントを参考にしながら簡単なデータ処理の方法のみご紹介します。不要な方は、このパートは読み飛ばしていただいても大丈夫です。

テーブルクラス作成

まずはデータベースにアクセスするためのテーブルクラスを作成します。下記のようにTableクラスを継承したTodoItemsを作成しました。int型はinteger()、String型はtext()のように指定します。nullable()()をつけることによってオプショナルにできます。TodoItemsを作ったら、@DriftDatabaseを作成しbuild_runnerを実行し、ファイルを自動生成します。

import 'package:drift/drift.dart';

part 'database.g.dart';

class TodoItems extends Table {
  IntColumn get id => integer().autoIncrement()();

  TextColumn get title => text().withLength(min: 6, max: 32)();

  TextColumn get content => text().named('body')();

  IntColumn get category => integer().nullable()();
}

class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;
}

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

レコードクラスの定義

次に、 データベースとのデータのやり取りに使用する、レコードクラスであるTodoItemsModelを定義しておきます。

@freezed
class TodoItemsModel with _$TodoItemsModel {
  const factory TodoItemsModel({
    required int id,
    required String title,
    String? content,
    String? category,
  }) = _TodoItemsModel;

  factory TodoItemsModel.fromJson(Map<String, Object?> json) =>
      _$TodoItemsModelFromJson(json);
}

データの追加・取得・削除

次に簡単なデータの挿入・取得・削除の方法の実装例を下記に示します。 それぞれの処理の関数は、データの追加をadd()、全てのデータ取得をgetAll()、全てのデータ削除をdeleteAll()としています。

class TodoItemsRepository {
  TodoItemsRepository({
    required this.database,
  });

  final AppDatabase database;


  Future<void> add(TodoItemsModel todoItemsModel) async {
    await database.into(database.todoItems).insert(todoItemsModel);
  }

  Future<List<TodoItemsModel>> getAll() async {
    final todoItemsList = await database.select(database.todoItems).get();

    return todoItemsList.map(
          (todoItems) {
        return TodoItemsModel(
          id: todoItems.id,
          title: todoItems.title,
          content: todoItems.content,
          category: todoItems.category,
        );
      },
    ).toList();
  }
  
  Future<void> deleteAll() async {
    await database.delete(database.todoItems).go();
  }
}

Driftを使ってつまづいたポイント

Listが保存できない

Driftはデータを保存する際に、int、Stringのような基本的な型しか保存できない問題があり、Listなどはそのまま保存できません。(ドキュメント参照)

List<String>型のデータの保存・復元

List<String>型のデータに関しても、直接テーブルクラスで定義できません。そのためList<String>→Stringという形で変換します。

保存

listDataのようなList<String>のリストがあったとします。

final listData = [
  'Data1',
  'Data2',
  'Data3',
  'Data4',
  'Data5',
];

これを以下のように、カンマ区切りでString型に変換します。

final stringData = listData.split(',');

listDataを含むデータクラスをextensionで事前にList<String>からString型に変換しておくとDBへの保存がスムーズになります。SampleDataからNewSampleDataへ変換する実装例はこちらです。

@freezed
class SampleData with _$SampleData {
  const factory TodoItemsModel({
    List<String>? listData,
    int? intData,
  }) = _SampleData;

  factory SampleData.fromJson(Map<String, Object?> json) =>
      _$SampleDataFromJson(json);
}

@freezed
class NewSampleData with _$NewSampleData {
  const factory NewSampleData({
    String stringData,
    int? intData,
  }) = _NewSampleData;
}

extension SampleDataExtensions on SampleData {
  NewSampleData convertToListData() {
    final newListData = listData?.split(',') ?? '';
    return NewSampleData(stringData: newListData, intData: intData);
  }
}

これでString型としてローカルDBへ保存できます。

復元

先ほどのカンマ区切りのstringDataの値は以下のようになっています。

print(stringData);
//'Data1,Data2,Data3,Data4,Data5'

stringDataをList<String>に復元する変換が下記のコードになります。

final newStringData = stringData?.join(',') ?? ""; 

extensionでSampleDataにコンバートした形が下記のコードです。

extension NewSampleDataExtensions on NewSampleData {
  NewSampleData convertToStringData() {
    final newStringData = stringData?.join(',') ?? "";
    return SampleData(listData: newStringData, intData: intData);
  }
}

このようにすると、問題なくDriftに保存前のデータを同じ形で取得できます!

端末の画像データを保存・復元

弊社ではオフラインで保存するデータの一部に画像データが含まれているため、その際にDriftを用いています。画像データをString型へ変換する方法が2通りあり、どちらを採用するか迷ったので判断基準と実装例を合わせてご紹介します!

保存

画像をDriftでローカルDBへ保存するにはString型にしないといけません。 画像データをStringで保存して復元する方法は次の2つです。

Uint8Listへ変換する方法は、画像データがデータベースに直接保存されるため容量が大きくなってしまいます。 pathから取得する場合デバイスのストレージを活用しているため、データベースのサイズを節約できます。 しかし、pathはデバイスのストレージから削除された場合、画像データが取得できないというデメリットがあります。 弊社の場合、画像データが相対的に必要な要素ではないため、pathを保存する方法が適していると判断し、そちらを採用しました。

画像のデータに関してはImagePicker()を使えば、端末のアルバムの情報を取ってこれます。その画像データをpathのStringへ変換してからデータベースに追加するという手順です。

Future getImageFile() async {
  final picker = ImagePicker();
  final pickedImage = await picker.pickImage(
    source: ImageSource.gallery,
  );

  if (pickedImage == null) {
    return null;
  }
  final imagePath = pickedImage.path;
  return imagePath;
}

このようにすると、String型としてそのまま保存できます。

imagePathの中身は

/data/user/.../example.jpg

のようになっています!

復元

ローカルDBから画像データのパスを取得後の復元のコードは下記になります。

final imageFile = File(imagePath);
final imageData = Image.file(imageFile);

File()で復元可能で、 復元後はImage.file()で画像を表示できます。

まとめ

今回はローカルDBに画像データとList<String>を保存する方法をご紹介しました。 どちらも保存の仕方ではなく、保存前のデータ加工が重要でした。

Driftで保存するためには「どのようなデータに変換したら復元しやすいか」という観点で開発していただけると幸いです。