🔥

痒い所に手が届くRealmObjectのflow拡張関数を作ろう

2023/06/15に公開

株式会社THIRDでは、スマホアプリデータベースにRealmを採用しています。
一般的に、バックグラウンドスレッドでRealmQueryを使ってデータソース層からFlowを使ってViewに情報を流していくとき、
findAllAsync()関数とfindFirstAsync()を使っていくことになると思います。
githubで見つけたサンプルアプリ
しかし、この2つの関数、それぞれ微妙に使いづらい点があります。

findAllAsync()の使いづらい点

Returns:
immediately an empty RealmResults. Users need to register a listener RealmResults.addChangeListener(RealmChangeListener) to be notified when the query completes.

とドキュメントにもあるとおり、findAllAsync()を使うとクエリ結果に関わらずまず空のRealmResults<T>オブジェクトが返却されます。ListView等を構築する際点滅したりする原因にもなりえますし、実際にクエリ結果が0件だった時と区別がつかないのも厄介です。
そこで、次の拡張関数を定義します。

fun <T : RealmObject> Flow<RealmResults<T>>.ignoreOnceEmptyResults(): Flow<RealmResults<T>> =
        flow {
            var ignoreEmptyResults = true
            collect { results ->
                if (ignoreEmptyResults && results.isEmpty()) {
                    ignoreEmptyResults = false
                } else {
                    emit(results)
                }
            }
        }

初回返却の空のRealmResults<T>をemitしないように回避します。
実際の運用ではフラグを渡して

if (ignoreOnceEmptyResults) {
    query.findAllAsync().toFlow().ignoreOnceEmptyResults()
} else {
    query.findAllAsync().toFlow()
}

というように使っています。

findFirstAsync()の使いづらい点

Returns:
immediately an empty RealmObject with isLoaded() == false. Trying to access any field on the returned object before it is loaded will throw an IllegalStateException.

とドキュメントにもあるとおり、findFirstAsync()を使うとクエリ結果に関わらずまずisLoaded() == falseのRealmObjectオブジェクトが返却されます。この状態で対象のオブジェクトにアクセスすると
\textcolor{red}{java.lang.IllegalStateException: Object is no longer managed by Realm. Has it been deleted?}
の例外が発生してしまいます。アプリクラッシュの原因になってしまうので、次の拡張関数を定義します。

fun <T : RealmObject> Flow<T?>.waitResultValid(): Flow<T?> = flow {
        collect { result ->
            result?.let {
                if (it.isLoaded && it.isValid) {
                    emit(it)
                } else {
                    emit(null)
                }
            }
        }
    }

実運用では、以下のように使います。

query.findFirstAsync().toFlow().waitResultValid()

さらにStateラッパーを挟んで、emitしない例外の時はLoading状態にする、みたいな運用もできそうです。

version情報
implementation "io.realm:realm-gradle-plugin:10.8.0"

株式会社THIRD エンジニアブログ

Discussion