Experiments Never Fail

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に搭載されていたり、有志のライブラリで実現できるのかも知れませんが、自作でもどうにかなりますよ、というお話でした。

最後に書いてある通りですがkotlinx-coroutines-play-servicesでいけますね👀 ただこうやって拡張はやして対応していけるのはいいですね👍 https://t.co/eSHYXtEWaP

— takahirom (@new_runnable) 2018年10月19日

やっぱりあったー!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"  ←追加
published at tags: Firebase Kotlin Android