【Flutter/Dart】Realm DBの導入と実装方法!

【Flutter/Dart】Realm DBの導入と実装方法!

この記事からわかること

  • Flutter/Dartshared_preferences実装方法
  • ローカルデータ保存するには?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Flutterアプリで端末(ローカル)にデータを永続的に保存する方法

FlutterでiOS/Androidアプリを開発する際に端末(ローカル)にデータを永続的に保存する方法はいくつか用意されています。

それぞれに一長一短がありますが今回はRealm DBを使用する方法をまとめていきます。

Realm DB

Realm

Realm(レルム)iOSやAndroidなどのモバイル向けに開発されたクラスプラットフォームデータベースです。

Realmの特徴はデバイス内にデータベースを作成して使用することです。デバイス自体にデータを保存するためオフライン環境での使用も可能になっており、アプリを停止した場合にもデータが保持されているので再度起動した場合には保存されたデータを使用することが可能になっています。

対応している言語

Realmではデータベース操作(CRUD処理)をSQL文を使用することなくクラス(オブジェクト)を操作するようにデータベースを操作することができるのが大きな特徴です。この基本的な特徴は共通なので詳細は以下の「Realm Swift」の記事も参考にしてみてください。

Realm Flutterの導入方法

pub.dev:realm

Realm FlutterはiOS、Android、Windows、MacOS、Linuxのプラットフォームをサポートしているため、異なるプラットフォームでの動作が保証されています。導入にはFlutter3.10.2以降が必要なので注意してください。

FlutterでRealmを利用できるようにするために以下のコマンドを実行してパッケージを導入します。

$ flutter pub add realm

これでパッケージの導入が完了し、import文を追加すれば使用できるようになります。

import 'package:realm/realm.dart';

データベーステーブルモデルクラス定義

Realmデータベースを扱う上でまずはデータベーステーブルとなるモデルクラスの定義が必要になります。@RealmModel()アノテーションを付与したモデルクラスを作成しておきます。さらにパーツファイルの宣言が必要になります。このパーツファイルは後述するコマンドを実行することで自動生成されるファイルになっており、ここではpart 'ファイル名.realm.dart';のように先に記述しておきます。


import 'package:realm/realm.dart';

// 自動生成されるコード
part 'shop.realm.dart';

// @RealmModel()アノテーションを付与したクラスがデータベーステーブルとなるモデルクラスとして認識される
@RealmModel()
class _Shop { // クラスの接頭辞には_(アンダースコア)を付与する
  // @PrimaryKey()で主キーとして定義
  @PrimaryKey()
  late String id;
  // 店舗名
  late String name; 
}

モデルの定義が記述できたらdart run realm generateコマンドを実行してファイル名.realm.dartファイルを自動生成しておきます。生成されるのは対象のファイルと同階層です。

$ dart run realm generate

[INFO] Generating build script...
[INFO] Generating build script completed, took 205ms

[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 76ms

[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 713ms

[INFO] Running build...
[INFO] 1.5s elapsed, 34/35 actions completed.
[INFO] realm:realm_generator on lib/Models/shop.dart:[generate (0)] completed, took 24ms
[WARNING] realm:realm_generator on lib/Models/shop.dart:
shop.realm.dart must be included as a part directive in the input library with:
    part 'shop.realm.dart';
[INFO] Running build completed, took 1.6s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 42ms

[INFO] Succeeded after 1.7s with 34 outputs (69 actions)

自動生成されるファイルは以下のような中身になっています。公式ドキュメントにはこのファイルもコミットに含めることが推奨されています。


// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'shop.dart';

// **************************************************************************
// RealmObjectGenerator
// **************************************************************************

// ignore_for_file: type=lint
class Shop extends _Shop with RealmEntity, RealmObjectBase, RealmObject {
  Shop(
    String id,
    String name,
  ) {
    RealmObjectBase.set(this, 'id', id);
    RealmObjectBase.set(this, 'name', name);
  }

  Shop._();

  @override
  String get id => RealmObjectBase.get<String>(this, 'id') as String;
  @override
  set id(String value) => RealmObjectBase.set(this, 'id', value);

  @override
  String get name => RealmObjectBase.get<String>(this, 'name') as String;
  @override
  set name(String value) => RealmObjectBase.set(this, 'name', value);

  @override
  Stream<RealmObjectChanges<Shop>> get changes =>
      RealmObjectBase.getChanges<Shop>(this);

  @override
  Stream<RealmObjectChanges<Shop>> changesFor([List<String>? keyPaths]) =>
      RealmObjectBase.getChangesFor<Shop>(this, keyPaths);

  @override
  Shop freeze() => RealmObjectBase.freezeObject<Shop>(this);

  EJsonValue toEJson() {
    return <String, dynamic>{
      'id': id.toEJson(),
      'name': name.toEJson(),
    };
  }

  static EJsonValue _toEJson(Shop value) => value.toEJson();
  static Shop _fromEJson(EJsonValue ejson) {
    if (ejson is! Map<String, dynamic>) return raiseInvalidEJson(ejson);
    return switch (ejson) {
      {
        'id': EJsonValue id,
        'name': EJsonValue name,
      } =>
        Shop(
          fromEJson(id),
          fromEJson(name),
        ),
      _ => raiseInvalidEJson(ejson),
    };
  }

  static final schema = () {
    RealmObjectBase.registerFactory(Shop._);
    register(_toEJson, _fromEJson);
    return const SchemaObject(ObjectType.realmObject, Shop, 'Shop', [
      SchemaProperty('id', RealmPropertyType.string, primaryKey: true),
      SchemaProperty('name', RealmPropertyType.string),
    ]);
  }();

  @override
  SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema;
}

Realmインスタンスの初期化

Realmを使用するためにはRealmインスタンスを生成する必要があります。このRealmインスタンスを使用してDBからの取得や追加などの操作を行います。初期化する際にはConfigurationインスタンスを渡します。localメソッドの引数に、対象のモデルクラスにschemaプロパティが自動で定義されているのでスキーマを指定します。配列になっているので複数モデルクラスがある場合はリスト形式で渡します。

var config = Configuration.local([Shop.schema]);
var realm = Realm(config);

データの保存

データを保存したい場合realm.writeメソッドの中でaddメソッドを使用します。引数に保存対象のオブジェクを指定します。

final shop = Shop(ObjectId().toString(), name);
realm.write(() {
  realm.add(Shop(shop));
});

データの取得

データを取得したい場合realm.all<データ型>メソッドを使用します。Realmに保存されている対象のデータ型を全件取得することができます。

// 全件取得
final shops = realm.all<Shop>();
for (var shop in shops) {
  print(shop.name);
}

データの更新

データを更新したい場合realm.find<データ型>などでオブジェクトを取得した後にオブジェクトのプロパティをそのまま更新すればOKです。ただしwriteメソッドの中で実行する必要があるので注意してください。

realm.write(() {
  final shop = realm.find<Shop>(id);
  if (shop != null) {
    shop.name = newName;
  }
});

データの削除

データを削除したい場合realm.find<データ型>などでオブジェクトを取得した後にdeleteメソッドを使用します。こちらもwriteメソッドの中で実行する必要があるので注意してください。

realm.write(() {
  final shop = realm.find<Shop>(id);
  if (shop != null) {
    realm.delete(shop);
  }
});

管理クラスに切り出してみる

ローカル保存・取得の機能を管理用クラスとして切り出してみました。_instanceからシングルトンインスタンスを参照できるようになっています。全コードは「GitHub」に公開しているので参考にしてください。


import 'package:realm/realm.dart';
import 'package:testapp/Models/shop.dart';

class RealmRepository {
  static final RealmRepository _instance = RealmRepository._internal();
  late Realm _realm;

  factory RealmRepository() => _instance;

  RealmRepository._internal() {
    final config = Configuration.local([Shop.schema]);
    _realm = Realm(config);
  }

  List<Shop> getAllShops() {
    return _realm.all<Shop>().toList();
  }

  void addShop(String name) {
    final shop = Shop(ObjectId().toString(), name);
    _realm.write(() {
      _realm.add(shop);
    });
  }

  void deleteShop(String id) {
    final shop = _realm.find<Shop>(id);
    if (shop != null) {
      _realm.write(() {
        _realm.delete(shop);
      });
    }
  }

  void dispose() {
    _realm.close();
  }
}

UI部分は以下のように実装してみました。


import 'package:flutter/material.dart';
import '../Repository/realm_repository.dart';
import '../Models/shop.dart';

class ShopListPage extends StatefulWidget {
  const ShopListPage({super.key});

  @override
  _ShopListPageState createState() => _ShopListPageState();
}

class _ShopListPageState extends State<ShopListPage> {
  final RealmRepository _realmService = RealmRepository();

  @override
  Widget build(BuildContext context) {
    List<Shop> shops = _realmService.getAllShops();

    return Scaffold(
      appBar: AppBar(title: const Text('Shop List')),
      body: ListView.builder(
        itemCount: shops.length,
        itemBuilder: (context, index) {
          final shop = shops[index];
          return ListTile(
            title: Text(shop.name),
            trailing: IconButton(
              icon: const Icon(Icons.delete, color: Colors.red),
              onPressed: () {
                _realmService.deleteShop(shop.id);
                setState(() {}); // UIを更新
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddShopDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddShopDialog(BuildContext context) {
    final TextEditingController _controller = TextEditingController();

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text("Add Shop"),
          content: TextField(
            controller: _controller,
            decoration: const InputDecoration(hintText: "Enter shop name"),
          ),
          actions: [
            TextButton(
              onPressed: () {
                _realmService.addShop(_controller.text);
                setState(() {});
                Navigator.of(context).pop();
              },
              child: const Text("Add"),
            ),
          ],
        );
      },
    );
  }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

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

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index