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

この記事からわかること
- Flutter/Dartのshared_preferencesの実装方法
- ローカルにデータを保存するには?
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Koala
- Xcode:16.0
- Flutter:3.29.2
- Dart:3.7.2
- Realm:20.0.1
- Mac M1:Sonoma 14.6.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);
}
});
管理クラスに切り出してみる
ローカル保存・取得の機能を管理用クラスとして切り出してみました。_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"),
),
],
);
},
);
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。