Firebase realtime snapshot listener using Coroutines

后端 未结 4 1691
轻奢々
轻奢々 2020-12-29 10:22

I want to be able to listen to realtime updates in Firebase DB\'s using Kotlin coroutines in my ViewModel.

The problem is that whenever a new message is created in t

相关标签:
4条回答
  • 2020-12-29 10:42

    This is working for me:

    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()
        }
    }
    

    Then you can use it like:

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

    If the observer block throws an error, or the block finishes, or your coroutine is cancelled, the listener is removed automatically.

    0 讨论(0)
  • 2020-12-29 10:44

    What I ended up with is I used Flow which is part of coroutines 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()
        }
    } 
    

    And that's how I observe it in my ViewModel

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

    more on topic https://medium.com/@elizarov/cold-flows-hot-channels-d74769805f9

    0 讨论(0)
  • 2020-12-29 11:01

    Extension function to remove callbacks

    For Firebase's Firestore database there are two types of calls.

    1. One time requests - addOnCompleteListener
    2. Realtime updates - addSnapshotListener

    One time requests

    For one time requests there is an await extension function provided by the library org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X. The function returns results from addOnCompleteListener.

    For the latest version, see the Maven Repository, kotlinx-coroutines-play-services.

    Resources

    • Using Firebase on Android with Kotlin Coroutines by Joe Birch
    • Using Kotlin Extension Functions and Coroutines with Firebase by Rosário Pereira Fernandes

    Realtime updates

    The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state. This is important because the function is called when the user's main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.

    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))
        })
    }
    

    In order to handle errors the try/catch pattern is used.

    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}")))
                }
    }
    
    0 讨论(0)
  • 2020-12-29 11:05

    I have these extension functions, so I can simply get back results from the query as a Flow.

    Flow is a Kotlin coroutine construct perfect for this purposes. 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)
            }
    }
    

    The following is an example of how to use the above functions.

    @ExperimentalCoroutinesApi
    fun getShoppingListItemsFlow(): Flow<List<ShoppingListItem>> {
        return FirebaseFirestore.getInstance()
            .collection("$COLLECTION_SHOPPING_LIST")
            .getDataFlow { querySnapshot ->
                querySnapshot?.documents?.map {
                    getShoppingListItemFromSnapshot(it)
                } ?: listOf()
            }
    }
    
    // Parses the document snapshot to the desired object
    fun getShoppingListItemFromSnapshot(documentSnapshot: DocumentSnapshot) : ShoppingListItem {
            return documentSnapshot.toObject(ShoppingListItem::class.java)!!
        }
    

    And in your ViewModel class, (or your Fragment) make sure you call this from the right scope, so the listener gets removed appropriately when the user moves away from the screen.

    viewModelScope.launch {
       getShoppingListItemsFlow().collect{
         // Show on the view.
       }
    }
    
    0 讨论(0)
提交回复
热议问题