一行まとめ: kotlinx-coroutines-play-services を使おうね
Firebase Firestore の Android 用 SDK では、データの取得はコールバックスタイルで行うようです。
また、コード例が Java のみで Kotlin の例がないので、Java のコード例を Kotlin で書き換えたあと、さらに Kotlin-coroutine を使って async/await 化してみます。
まず、単一のドキュメントを取得する方法です。
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());
}
}
});
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())
}
}
少しシンプルになりました。
さて、ここからが本題で、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
が使えます。
ただしコレクションの場合の T
は QuerySnapshot
型です。
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.gradle
に kotlinx-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" ←追加