KMPでRealmを使ってみる

採用理由

単語帳を作りたいのでデータ永続化のためにデータベースを探していました。
なんやかんやあってRealm[1]を採用することになりました。
採用理由は

kotlin android database おすすめ

で調べて最初に出てきた記事[2]でおすすめされていたライブラリだからです。
英語で

kotlin multiplatform database recommend

と調べても、一番上の質問板[3]ではRealmがおすすめされています。だいぶおすすめっぽいです。
Android公式のRoom[4]も使ってみたかったのですが、ページトップに書いてあるとおりAndroidJetpackの一部とのことで、 めちゃめちゃAndroidのなのでkmpで利用するには適さないと考えました。

ボタン作成

自分用のテンプレリポジトリがあるのでこいつに実装します。
動作もわかりやすいので、適当にボタンをおいてカウンタを実装しました。 ボタンをタップしたらボタン内部の数字が1大きくなります。
該当差分はこちらです。Android公式のMVVMに従って実装したつもりです。
koinを導入してあるので使ってもよいのですが、例としてわかりにくくなるので今回はなしです。
動作はこんな感じ。

カウントアップボタンのgif

ライブラリ導入

これはまだ永続化していないので、一度アプリを落とすとカウントがリセットされてしまいます。これを永続化するためにRealmを導入します。
とりあえず依存関係の追加をおこないました。PRはこちらです。


gradle/libs.versions.toml

[versions]
~~ 省略 ~~
realm="1.16.0"

[libraries]
~~ 省略 ~~
realm-library-base={module="io.realm.kotlin:library-base" ,version.ref="realm"}

[plugins]
~~ 省略 ~~
realm = {id = "io.realm.kotlin",version.ref ="realm"}

プラグインとライブラリの依存関係を追加します。
バージョンは『Kotlin SDK のインストール』[5]の数字を参考にしました。


build.gradle.kts


alias(libs.plugins.realm) apply false
        


composeApp/build.gradle.kts


plugins {
    ~~ 省略 ~~
    alias(libs.plugins.realm) apply false
}

kotlin{
    ~~ 省略 ~~
    
    sourceSet{
        ~~ 省略 ~~

        commonMain.dependencies {
            ~~ 省略 ~~
        
            implementation(libs.realm.library.base)
        
            ~~ 省略 ~~
        }
        ~~ 省略 ~~
    }
    ~~ 省略 ~~
}
        

プロジェクト配下とApp配下のgradleに追加したライブラリ・プラグインを追加します。

利用

作ったリポジトリの内部で永続化を行います。差分はこちらです。
ここからはクイックスタート[6]を参考に実装を行います


composeApp/src/commonMain/kotlin/domain/Count.kt

package domain

import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import org.mongodb.kbson.ObjectId

class Counter : RealmObject {
    @PrimaryKey
    var _id: ObjectId = ObjectId()
    var count: Int = 0
}

『オブジェクトモデルを定義する』[7]を参考に保存したいオブジェクトを定義しました。
保存したいのは数値だけなので、PrimaryKeyとcountだけ用意しました。
repositoryの各行の説明に入ります。

    private val config: RealmConfiguration = RealmConfiguration.create(schema = setOf(Counter::class))
    private val realm: Realm = Realm.open(config)

『Realmを開く』[8]を参考にCounterクラスを読み込みます。
今回利用したいものはCounterなので、schemaにはCounterだけ追加します。

    private val counter: Counter = realm.query<Counter>().first().find() ?:
        realm.writeBlocking {
            copyToRealm(
                Counter()
                )
            }
    


『オブジェクトの作成、読み取り、更新、削除』[9]を参考に読み書きをします。
エルビス演算子(?:)の前で、queryを利用してCounter のデータを読み出します。1つしか保存していないので検索ワードはなしで、そのまま取り出します。
エルビス演算子以降は書き込みを参考にデータの新規作成を行います。 初回起動時はデータが保存されておらず、queryの結果がnullになるので 書き込み処理を入れてあげます。

    findLatest(counter)?.apply {
            count = value
        }
    }

値を更新した際に、同時にDBも更新しています。
読み出しているcounterインスタンスの値を更新します。
参考にしたのは引き続き、『オブジェクトの作成、読み取り、更新、削除』[9]です。

    init{
        count = counter.count
    }

大したことはしてなく、初回表示を0ではなくしただけです。

完成

以上で完成です。
DBに保存して永続化しているので、アプリを終了しても数値を継続利用できます。
全体の差分はこちらです

DBを使って保存しているgif

今後

公式ページ[10]や公式のgithub[11]を見ていると、Atlas Device Sync + Realm SDKsの組み合わせが2024/09にdeprecatedになったようです。この作業は10月に入ってから始めたのでほんとに直前に発表されています。タイムリーです。
警告に従うならRealm Kotlinの3.0.0以上のバージョンを利用するか、syncの機能の利用を避けなくてはなりません。
Atlas Device Syncについて調べると、デバイス間で同期をとる機能のよう[12]です。今回の実装だとあまり関係ないですね。
でもせっかくなのでバージョンアップしたい気持ちがあります。1.16.0から3.0.0にアップデートするのは大変です。一気にメジャーバージョンが2個も上がることあるんですね……。
現状のコードではバージョンを3.0.0にすると以下のエラーが発生してしまいます。

kmpTemplate/composeApp/src/commonMain/kotlin/domain/Count.kt:-1:-1
Details: Internal error in file lowering : io.realm.kotlin.compiler.RealmModelLowering: Found interface org.jetbrains.kotlin.ir.declarations.IrFactory, but class was expected

(2024/10/10現在、)このエラーコードで検索をかけても8件しかヒットしませんでした。 調べても沼りそうなので今回は引き返すことにします。どこかで対策したいです。
と、まぁ今後の目標もたったところで今回はこの辺で終わります。

参考

  1. Realm
    https://github.com/realm
  2. KotlinでRealm(他のDBとの概要比較)
    https://qiita.com/atsuuu/items/357cbd3b97a6573b034c
  3. Kotlin Multiplatform Project - Which is the best database available ?
    https://www.reddit.com/r/Kotlin/comments/9skoh6/kotlin_multiplatform_project_which_is_the_best/
  4. Room を使用してローカル データベースにデータを保存する
    https://developer.android.com/training/data-storage/room?hl=ja
  5. Kotlin SDK のインストール
    https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/sdk/kotlin/install/#install-the-kotlin-sdk
  6. クイック スタート - Kotlin SDK
    https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/sdk/kotlin/quick-start/#quick-start---kotlin-sdk
  7. クイック スタート - Kotlin SDK 『オブジェクトモデルを定義する』
    https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/sdk/kotlin/quick-start/#define-your-object-model
  8. クイック スタート - Kotlin SDK 『Realmを開く』 https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/sdk/kotlin/quick-start/#open-a-realm
  9. オブジェクトの作成、読み取り、更新、削除
    https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/sdk/kotlin/quick-start/#create--read--update--and-delete-objects
  10. Atlas Device SDKs の廃止
    https://www.mongodb.com/ja-jp/docs/atlas/device-sdks/deprecation/
  11. realm-kotlin
    https://github.com/realm/realm-kotlin
  12. クラスの同期構成ベース
    https://www.mongodb.com/ja-jp/docs/realm-sdks/dotnet/latest/reference/Realms.Sync.SyncConfigurationBase.html