【Flutter/Dart】Realm DBの導入と実装方法!
この記事からわかること
- Flutter/Dartのshared_preferencesの実装方法
- ローカルにデータを保存するには?
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Otter 2 Feature Drop
- Android OS:15以降
- Kotlin:2.2.21
- Xcode:26.0.1
- iOS:26
- Swift:6
- Flutter:3.38.3
- Dart:3.10.1
- Realm:20.0.1
- macOS(M1):Tahoe 26.0.1
Flutterアプリで端末(ローカル)にデータを永続的に保存する方法
FlutterでiOS/Androidアプリを開発する際に端末(ローカル)にデータを永続的に保存する方法はいくつか用意されています。
- shared_preferences
- sqflite
- Realm
- flutter_secure_storage
- テキストファイル
それぞれに一長一短がありますが今回はRealm DBを使用する方法をまとめていきます。
Realm DB
Realm(レルム)はiOSやAndroidなどのモバイル向けに開発されたクラスプラットフォームデータベースです。
Realmの特徴はデバイス内にデータベースを作成して使用することです。デバイス自体にデータを保存するためオフライン環境での使用も可能になっており、アプリを停止した場合にもデータが保持されているので再度起動した場合には保存されたデータを使用することが可能になっています。
対応している言語
- Swift
- Objective-C
- Java
- Kotlin
- C#
- JavaScript
Realmではデータベース操作(CRUD処理)をSQL文を使用することなくクラス(オブジェクト)を操作するようにデータベースを操作することができるのが大きな特徴です。この基本的な特徴は共通なので詳細は以下の「Realm Swift」の記事も参考にしてみてください。
Realm Flutterの導入方法
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);
}
});
マイグレーション処理
公式リファレンス:Realm オブジェクト スキーマの更新 - Flutter SDK
既存でリリース済みのデータ構造に対してプロパティなどを追加したい場合は「マイグレーション処理」が必要になります。基本的に行わないといけないのは「スキーマ番号のインクリメント」です。Realmのマイグレーションではプロパティの追加 or 削除に関してはスキーマ番号の更新だけで自動的に移行されるようになっていますが、プロパティ名のリネームやその他複雑なマイグレーションに関しては「手動移行処理」を実行する必要があります。
例えば_Shopに「チェーン店かどうか」を識別するためのlate String isChainを追加する場合で考えてみます。まずはモデルクラスにプロパティを追加します。
class _Shop {
@PrimaryKey()
late String id;
/// 店舗名
late String name;
/// チェーン店かどうか
late String isChain;
}
続いて構造が変化したのでスキーマバージョンをインクリメントします。Configuration.localの引数schemaVersionでデフォルトが1なので2に上げておきます。また別のプロパティを追加する場合などはここを順番にあげていけばOKです。
final config = Configuration.local(
[Shop.schema],
schemaVersion: 2, // デフォルトが1なので2に上げておく
);
_realm = Realm(config);
最後にRealmのモデルクラスをアップデートするために以下のコマンドを実行します。削除する場合も同様の手順で大丈夫です。bool型の場合は初期値としてfalseが格納された状態になります。
$ dart run realm generate
プロパティなどをリネームしたい場合はmigrationCallback内で手動移行処理を実装します。renamePropertyでプロパティ名を変更できます。
final configWithRenamedAge = Configuration.local(
[Shop.schema],
schemaVersion: 3,
migrationCallback: ((migration, oldSchemaVersion) {
migration.renameProperty('Shop', 'name', 'storeName');
})
);
もしdart run realm generateを実行しているのに自動生成ファイルが更新されない場合はキャッシュが読み込まれている可能性があるので以下を実行してから再度実行してみてください。
$ dart run build_runner clean
管理クラスに切り出してみる
ローカル保存・取得の機能を管理用クラスとして切り出してみました。_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"),
),
],
);
},
);
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





