痒い所に手が届くRealmObjectのflow拡張関数を作ろう
株式会社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オブジェクトが返却されます。この状態で対象のオブジェクトにアクセスすると
の例外が発生してしまいます。アプリクラッシュの原因になってしまうので、次の拡張関数を定義します。
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"
Discussion