LoginSignup
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-19

一行まとめ: 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"  ←追加

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14