Java
Android
Kotlin
RxJava
Firebase

AndroidのFirebase Realtime DBでMoshi+Rxを使ってデータのマッピングをする

これは Android その2 Advent Calendar の13日目の記事です。

FirebaseのAdventカレンダーには漏れてしまったのでどうしようかなと思っていましたが、比較的Android寄りの記事なのでこちらに投稿します。

Firebase SDKの標準で用意されているgetValue

Android向けのFirebase SDKにはDataSnapshotクラスに getValue(Class<T> classType) というメソッドが用意されていて、簡単にデータからオブジェクトへのマッピングを行うことができます。

https://firebase.google.com/docs/reference/android/com/google/firebase/database/DataSnapshot.html#getValue%28java%2elang%2eClass%3cT%3e%29

リファレンスによると、このメソッドにはいくつか制限があります。

  • 対象のクラスが引数を持たないデフォルトコンストラクタを持っている必要がある
  • マッピングしたいフィールドはすべてパブリックなgetterメソッドを持っている必要がある

また、基本となる型以外の任意の型をフィールドとして持つことはできないのも割りと不便です。

例)Kotlinのデータクラスで、Date型をもたせたいパターンには使えない

data class User(
        val createdAt: Date,
        val updatedAt: Date,
        val name: String,
        val email: String? = null
)

パフォーマンス的にも微妙?

getValue(Class<T> classType) を使う場合、引数にクラス型を渡していて、おそらく内部では毎回リフレクションによるマッピングを行っているのでパフォーマンス的にもあまり良くなさそうです。
(実際どうなのかはSDKの内部が今のところ公開されてないのでわかりません、もしかするとリフレクション結果のキャッシュ等を行っている可能性もあります)

Moshiで自前のマッピング処理を実装する

上記の問題を回避するために、Moshiを使って自前でマッピングするようにしてみます。マッピング用のライブラリはMap型との相互変換ができればなんでもよかったのですが、Kotlinとの相性のいいMoshiを使用しています。

fun <T> DataSnapshot.toObject(adapter: JsonAdapter<T>): T? {
    return adapter.fromJsonValue(this.value)
}

DataSnapshot::value でMap型のデータが取得できるので、さくっと実装できます。

逆に、DBへの書き込みを行う場合は adapter.toJsonValue(object) の結果をセットしてあげればOKです。

DBのRxラッパーと組み合わせて汎用性アップ

Realtime DBはRxとの相性が良いのでRxラッパー経由で使うのがおすすめです。例えば、データ監視の処理は以下のようにRxでラップしておきます。

fun Query.observeData(): Observable<DataSnapshot> {
    return Observable.create<DataSnapshot> { emitter ->
        val listener = addValueEventListener(object: ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                emitter.onNext(snapshot)
            }
            override fun onCancelled(error: DatabaseError) {
                emitter.onError(error.toException())
            }
        })
        emitter.setCancellable { removeEventListener(listener) }
    }
}

さらに、 Observable<DataSnapshot> に先ほどのMoshiを使ったマッピング用メソッドを生やしてあげます

fun <T> Observable<DataSnapshot>.mapToObject(adapter: JsonAdapter<T>): Observable<T> {
    return mapNotNull { it.toObject(adapter) }
}

// nullな値を通さないようにするmapオペレータ
fun <T, R> Observable<T>.mapNotNull(mapper: (T) -> R?): Observable<R> {
    return filter { mapper(it) != null }.map(mapper)
}

こうしておけば、以下のように汎用的に使用できます。

data class User(
        val createdAt: Date,
        val updatedAt: Date,
        val name: String,
        val email: String? = null
)

val userAdapter = Moshi.Builder()
        .add(Date::class.java, Rfc3339DateJsonAdapter())
        .add(KotlinJsonAdapterFactory())
        .build()
        .adapter(User::class.java)

database.getReference("/user/...")
        .observeData()
        .mapToObject(userAdapter) // Observable<User>

以上です。