Firebase Firestore のデータを async/await で取得する

一行まとめ: kotlinx-coroutines-play-services を使おうね

Firebase Firestore の Android 用 SDK では、データの取得はコールバックスタイルで行うようです。

また、コード例が Java のみで Kotlin の例がないので、Java のコード例を Kotlin で書き換えたあと、さらに Kotlin-coroutine を使って async/await 化してみます。

データを1件取得する場合

まず、単一のドキュメントを取得する方法です。

Java 版

Java では次のコード例になります。

DocumentReference docRef = db.collection("cities").document("SF");
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DocumentSnapshot> task) {
        if (task.isSuccessful()) {
            DocumentSnapshot document = task.getResult();
            if (document.exists()) {
                Log.d(TAG, "DocumentSnapshot data: " + document.getData());
            } else {
                Log.d(TAG, "No such document");
            }
        } else {
            Log.d(TAG, "get failed with ", task.getException());
        }
    }
});

Kotlin 版

val docRef = db.collection("cities").document("SF")
docRef.get().addOnCompleteListener { task ->
    if (task.isSuccessful()) {
        val document = task.getResult()
        if (document.exists()) {
            Log.d(TAG, "DocumentSnapshot data: " + document.data)
        } else {
            Log.d(TAG, "No such document")
        }
    } else {
        Log.d(TAG, "get failed with ", task.getException())
    }
}

少しシンプルになりました。

Kotlin + async/await版

さて、ここからが本題で、async/await でデータを取得できるようにします。 注目したいのが、 docRef.get() の戻り値の型で、これは Task<T> です。 Task<T> に、 addOnCompleteListener やその他諸々のコールバックを受信するためのメソッドがあり、結果はそのコールバックで受け取ります。

ということは、この Task<T> を async/await で使える形式に変換してあげればよいわけです。

そこで、こんな拡張関数を作ってあげます。

suspend fun <T> Task<T>.toSuspendable(): T {
    return suspendCoroutine { cont ->
        this.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                cont.resume(task.result)
            } else if (task.isCanceled) {
                cont.resumeWithException(CancellationException())
            } else {
                cont.resumeWithException(task.exception ?: Exception("Unknown"))
            }
        }
    }
}

Kotlin で async/await = 所謂コルーチンに対応させるには、メソッドに suspend を付けます。そして、suspendCoroutine を呼び出すと、そこで実行を「一時停止」し、cont.resume または cont.resumeWithException が呼び出されたら再開します。ここでは addOnCompleteListener のコールバックを受信したときに cont.resume を呼び出して、処理を再開させています。

さて、実際に使ってみましょう。

launch(CommonPool) {
    val document = docRef.get().toSuspendable()
    if (document.exists()) {
        Log.d(TAG, "DocumentSnapshot data: " + document.data)
    } else {
        Log.d(TAG, "No such document")
    }
}

はい。 最初の Java のコードに比べるとずいぶんスッキリしたと思います。

データを複数取得する場合

作成した拡張関数 Task.toSuspendable は、データを複数件取得するときにも使えます。

例えば、以下の Java のコード例、

db.collection("cities")
        .whereEqualTo("capital", true)
        .get()
        .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            @Override
            public void onComplete(@NonNull Task<QuerySnapshot> task) {
                if (task.isSuccessful()) {
                    for (QueryDocumentSnapshot document : task.getResult()) {
                        Log.d(TAG, document.getId() + " => " + document.getData());
                    }
                } else {
                    Log.d(TAG, "Error getting documents: ", task.getException());
                }
            }
        });

これを、一気に Kotlin + async/await 化してみます。

launch(CommonPool) {
    val querySnapshot =  db.collection("cities")
        .whereEqualTo("capital", true)
        .get().toSuspendable()
    for (document in querySnapshot) {
        Log.d(TAG, document.id + " => " + document.data)
    }
}

複数件を取得する db.collection("cities").whereXXX(...).get() の戻り値も Task<T> なので toSuspendable が使えます。 ただしコレクションの場合の TQuerySnapshot 型です。 QuerySnapshot はそれ自体が複数件のドキュメントを持っているので、 for で走査することができます。

まとめ

コールバックスタイルの型を suspend 可能な関数に変換する拡張関数を作っておくと、スッキリと書けます。

もしかしたら既にFirebase SDKに搭載されていたり、有志のライブラリで実現できるのかも知れませんが、自作でもどうにかなりますよ、というお話でした。

やっぱりあったー!ww 自作の .toSuspendable() は、kotlinx-coroutines-play-services を導入したら .await() に置き換えられます。こっちの方が cancellable だし完了してる場合の考慮もされれてよいですね :thumbsup:

導入方法はアプリモジュールの build.gradlekotlinx-coroutines-play-services を追加、です。

def coroutines_version = '0.30.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_version"  ←追加