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

**一行まとめ： [kotlinx-coroutines-play-services](https://github.com/Kotlin/kotlinx.coroutines/tree/develop/integration/kotlinx-coroutines-play-services) を使おうね**

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

* [Cloud Firestore でデータを取得する  |  Firebase](https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja)

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

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

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

### Java 版

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 版

```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 で使える形式に変換してあげればよいわけです。

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

```kotlin
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`` を呼び出して、処理を再開させています。

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

```kotlin
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 のコード例、

```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 化してみます。

```kotlin
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`` が使えます。
ただしコレクションの場合の ``T`` は ``QuerySnapshot`` 型です。
``QuerySnapshot`` はそれ自体が複数件のドキュメントを持っているので、 ``for`` で走査することができます。

## まとめ

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

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

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">最後に書いてある通りですがkotlinx-coroutines-play-servicesでいけますね👀 ただこうやって拡張はやして対応していけるのはいいですね👍 <a href="https://t.co/eSHYXtEWaP">https://t.co/eSHYXtEWaP</a></p>&mdash; takahirom (@new_runnable) <a href="https://twitter.com/new_runnable/status/1053208499193241601?ref_src=twsrc%5Etfw">2018年10月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

```gradle
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"  ←追加
```