使用协程的Firebase实时快照侦听器

时间:2019-04-01 16:46:52

标签: android kotlin google-cloud-firestore kotlin-coroutines

我希望能够使用ViewModel中的Kotlin协程收听Firebase DB中的实时更新。

问题在于,每当在集合中创建新消息时,我的应用程序就会冻结,并且无法从此状态中恢复。我需要杀死它并重新启动应用程序。

它第一次通过,我可以在UI上看到以前的消息。当SnapshotListener第二次被调用时,会发生此问题。

我的observer()函数

val channel = Channel<List<MessageEntity>>()
firestore.collection(path).addSnapshotListener { data, error ->
    if (error != null) {
        channel.close(error)
    } else {
        if (data != null) {
            val messages = data.toObjects(MessageEntity::class.java)
            //till this point it gets executed^^^^
            channel.sendBlocking(messages)
        } else {
            channel.close(CancellationException("No data received"))
        }
    }
}
return channel

这就是我要观察消息的方式

launch(Dispatchers.IO) {
        val newMessages =
            messageRepository
                .observer()
                .receive()
    }
}

sendBlocking()替换为send()后,我仍然没有在该频道中收到任何新消息。 SnapshotListener端被执行

//channel.sendBlocking(messages) was replaced by code bellow
scope.launch(Dispatchers.IO) {
    channel.send(messages)
}
//scope is my viewModel

如何使用Kotlin协程在firestore / realtime-dbs中观察消息?

4 个答案:

答案 0 :(得分:3)

扩展功能可删除回调

对于Firebase的Firestore数据库,有两种类型的调用。

  1. 一次请求-addOnCompleteListener
  2. 实时更新-addSnapshotListener

一次请求

有一次请求,库await提供了org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X扩展功能。该函数从addOnCompleteListener返回结果。

资源

实时更新

扩展功能awaitRealtime进行检查,包括验证continuation的状态,以查看其是否处于isActive状态。这一点很重要,因为当用户的主要内容供稿通过生命周期事件,手动刷新供稿或从其供稿中删除内容时,将调用该函数。没有此检查,将导致崩溃。

ExtenstionFuction.kt

data class QueryResponse(val packet: QuerySnapshot?, val error: FirebaseFirestoreException?)

suspend fun Query.awaitRealtime() = suspendCancellableCoroutine<QueryResponse> { continuation ->
    addSnapshotListener({ value, error ->
        if (error == null && continuation.isActive)
            continuation.resume(QueryResponse(value, null))
        else if (error != null && continuation.isActive)
            continuation.resume(QueryResponse(null, error))
    })
}

为了处理错误,使用了try / catch模式。

Repository.kt

object ContentRepository {
    fun getMainFeedList(isRealtime: Boolean, timeframe: Timestamp) = flow<Lce<PagedListResult>> {
        emit(Loading())
        val labeledSet = HashSet<String>()
        val user = usersDocument.collection(getInstance().currentUser!!.uid)
        syncLabeledContent(user, timeframe, labeledSet, SAVE_COLLECTION, this)
        getLoggedInNonRealtimeContent(timeframe, labeledSet, this)        
    }
    // Realtime updates with 'awaitRealtime' used
    private suspend fun syncLabeledContent(user: CollectionReference, timeframe: Timestamp,
                                       labeledSet: HashSet<String>, collection: String,
                                       lce: FlowCollector<Lce<PagedListResult>>) {
        val response = user.document(COLLECTIONS_DOCUMENT)
            .collection(collection)
            .orderBy(TIMESTAMP, DESCENDING)
            .whereGreaterThanOrEqualTo(TIMESTAMP, timeframe)
            .awaitRealtime()
        if (response.error == null) {
            val contentList = response.packet?.documentChanges?.map { doc ->
                doc.document.toObject(Content::class.java).also { content ->
                    labeledSet.add(content.id)
                }
            }
            database.contentDao().insertContentList(contentList)
        } else lce.emit(Error(PagedListResult(null,
            "Error retrieving user save_collection: ${response.error?.localizedMessage}")))
    }
    // One time updates with 'await' used
    private suspend fun getLoggedInNonRealtimeContent(timeframe: Timestamp,
                                                      labeledSet: HashSet<String>,
                                                      lce: FlowCollector<Lce<PagedListResult>>) =
            try {
                database.contentDao().insertContentList(
                        contentEnCollection.orderBy(TIMESTAMP, DESCENDING)
                                .whereGreaterThanOrEqualTo(TIMESTAMP, timeframe).get().await()
                                .documentChanges
                                ?.map { change -> change.document.toObject(Content::class.java) }
                                ?.filter { content -> !labeledSet.contains(content.id) })
                lce.emit(Lce.Content(PagedListResult(queryMainContentList(timeframe), "")))
            } catch (error: FirebaseFirestoreException) {
                lce.emit(Error(PagedListResult(
                        null,
                        CONTENT_LOGGED_IN_NON_REALTIME_ERROR + "${error.localizedMessage}")))
            }
}

答案 1 :(得分:2)

我具有这些扩展功能,因此我可以简单地从查询中以流的形式获取结果。

Flow是可用于此目的的Kotlin协程构建体。 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/

@ExperimentalCoroutinesApi
fun CollectionReference.getQuerySnapshotFlow(): Flow<QuerySnapshot?> {
    return callbackFlow {
        val listenerRegistration =
            addSnapshotListener { querySnapshot, firebaseFirestoreException ->
                if (firebaseFirestoreException != null) {
                    cancel(
                        message = "error fetching collection data at path - $path",
                        cause = firebaseFirestoreException
                    )
                    return@addSnapshotListener
                }
                offer(querySnapshot)
            }
        awaitClose {
            Timber.d("cancelling the listener on collection at path - $path")
            listenerRegistration.remove()
        }
    }
}

@ExperimentalCoroutinesApi
fun <T> CollectionReference.getDataFlow(mapper: (QuerySnapshot?) -> T): Flow<T> {
    return getQuerySnapshotFlow()
        .map {
            return@map mapper(it)
        }
}

以下是如何使用上述功能的示例。

@ExperimentalCoroutinesApi
fun getShoppingListItemsFlow(): Flow<List<ShoppingListItem>> {
    return FirebaseFirestore.getInstance()
        .collection("$COLLECTION_SHOPPING_LIST")
        .getDataFlow { querySnapshot ->
            querySnapshot?.documents?.map {
                getShoppingListItemFromSnapshot(it)
            } ?: listOf()
        }
}

在您的ViewModel类(或您的Fragment)中,请确保您在正确的范围内调用此函数,以便当用户离开屏幕时可以适当地删除侦听器。

viewModelScope.launch {
   getShoppingListItemsFlow().collect{
     // Show on the view.
   }
}

答案 2 :(得分:1)

我最终使用的是Flow,它是协程1.2.0-alpha-2的一部分

return flowViaChannel { channel ->
   firestore.collection(path).addSnapshotListener { data, error ->
        if (error != null) {
            channel.close(error)
        } else {
            if (data != null) {
                val messages = data.toObjects(MessageEntity::class.java)
                channel.sendBlocking(messages)
            } else {
                channel.close(CancellationException("No data received"))
            }
        }
    }
    channel.invokeOnClose {
        it?.printStackTrace()
    }
} 

这就是我在ViewModel中观察到的方式

launch {
    messageRepository.observe().collect {
        //process
    }
}

有关主题https://medium.com/@elizarov/cold-flows-hot-channels-d74769805f9

的更多信息

答案 3 :(得分:0)

这对我有用:

suspend fun DocumentReference.observe(block: suspend (getNextSnapshot: suspend ()->DocumentSnapshot?)->Unit) {
    val channel = Channel<Pair<DocumentSnapshot?, FirebaseFirestoreException?>>(Channel.UNLIMITED)

    val listenerRegistration = this.addSnapshotListener { value, error ->
        channel.sendBlocking(Pair(value, error))
    }

    try {
        block {
            val (value, error) = channel.receive()

            if (error != null) {
                throw error
            }
            value
        }
    }
    finally {
        channel.close()
        listenerRegistration.remove()
    }
}

然后您可以像使用它一样

docRef.observe { getNextSnapshot ->
    while (true) {
         val value = getNextSnapshot() ?: continue
         // do whatever you like with the database snapshot
    }
}

如果观察者块抛出错误,或者该块结束,或者您的协程被取消,则侦听器将自动删除。