ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

RealmのCoroutines対応で気をつけたこと

こんにちは、tkyです。

「Realmのtransaction処理、Coroutinesで書きたいな〜」

そう思ったことありませんか?ありますよね。

最新のRealm(10.0)ではkotlin拡張がいくつか存在して executeTransactionAwait を使うと時間のかかる書き込み処理などをsuspend funで記述することができるようになります。

docs.mongodb.com

こちらでRxで書いていた処理をCotourinesで書き換える事ができるようになりますが、いくつか問題も出てきたのでその辺りをこのブログで共有できればと思います。

出てきた問題

  • UIスレッドで処理できない(初期化時に明示的にuiスレッド許可する必要あり)
  • 異なるスレッドでRealmインスタンスを生成できない

UIスレッドで処理できない

RealmException if called from the UI thread, unless an explicit opt-in has been declared in {@link RealmConfiguration.Builder#allowWritesOnUiThread(boolean)}.

対策その1ー UIスレッドの操作を許可する

Realmインスタンスを作る時にBuilderにallowWritesOnUiThreadを指定するだけです。一応これで解決自体はできますが、

UIスレッドを止めてしまうことになるのでパフォーマンスに気をつけたいところです。

val realmConfiguration = RealmConfiguration.Builder()
    .allowWritesOnUiThread(true)
    .build()

対策その2ー Realm処理用のコルーチンディスパッチャを用意する

UIスレッドを止めないようにRealmを扱うための専用のスレッドとコルーチンディスパッチャを用意する方法があります。

シングルスレッドで動作できるコルーチンディスパッチャを以下のように定義します。

// これをDaggerなどを使ってDIすると良いでしょう
fun providesRealmDispatcher(): CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

// 関数にwithContextを使ってRealmを処理するスレッドをディスパッチャで指定する
override suspend fun write(value: String) = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.executeTransactionAwait {
            it.copyToRealmOrUpdate(
                Hoge.create(
                    value
                )
            )
        }
    }
}

一応これで書き込み時にUIスレッドで処理できない問題は解決するのですが、別の問題も現れてきました。

異なるスレッドでRealmインスタンスを生成できない

こんなエラーが出てきます

java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.

これはWrite処理はsuspend関数対応しましたが、Read処理については通常の関数として処理してしまったことで

  • Read処理が走る・・・ViewModelScopeのIOスレッドで最初にRealmインスタンスが作られる
  • Write処理が走る・・・Realm用のディスパッチャスレッドからRealmインスタンスにアクセスする

この時RealmインスタンスはViewModelScopeのIOディスパッチャで作られたものです。

それを別のスレッド(Realm用のスレッドから作られたディスパッチャ)からアクセスしようとしたために発生したものでした

そのためWrite処理だけでなくRead処理も含めてRealm処理用のコルーチンディスパッチャ上で動作させる必要があります

この問題についてはStackOverFlowでもちょくちょくみる内容となっています。

stackoverflow.com

// 書き込むデータはString型としています

override suspend fun read(key: String): String = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.where(Hoge::class.java).equalTo("key", key).findFirst()?.value
    }
}

override suspend fun write(value: String) = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.executeTransactionAwait {
            it.copyToRealmOrUpdate(
                Hoge.create(
                    value
                )
            )
        }
    }
}

まとめ

Realmをコルーチンで記述するときは

  • 専用のDispacherを作成して
  • Read処理もWrite処理も全てコルーチン化すると良い

それでは良いKotlinライフを。