1

I have a DAO class where I have fetchHubList method which fetches a collection of documents from cloud Firestore asynchronously using await(). This implementation used the "get()" method which I got to know later on does not fetch real-time updates. On trying to implement the code similarly using onSnapshotListener gives an error (which was quite expected to be honest, because get() and this methods return quite different things). Does anyone have any idea how to implement this?

How the code is currently:

 suspend fun fetchHubList(): ArrayList<HubModel>? = try {
        val hubList = ArrayList<HubModel>()

        hubsListCollection.get().await().map { document ->

            if (document != null) {

                Log.d(TAG, "Data fetch successful!")
                Log.d(TAG, "the document id is ${document.id}")

                val temp = HubModel(document.get("hubName").toString(),
                                    document.id.toString(),
                        document.get("isAdmin") as Boolean)
                hubList.add(temp)

                     //   hubList.add(document.toObject(HubModel::class.java))

            } else {
                Log.d(TAG, "No such document")
            }

        }

And what I want to implement here (and which is totally erroneous):

suspend fun fetchHubList(): ArrayList<HubModel>? = try {
        val hubList = ArrayList<HubModel>()

        hubsListCollection.addSnapshotListener().await().map { document ->

            if (document != null) {

                Log.d(TAG, "Data fetch successful!")
                Log.d(TAG, "the document id is ${document.id}")

                val temp = HubModel(document.get("hubName").toString(),
                                    document.id.toString(),
                        document.get("isAdmin") as Boolean)
                hubList.add(temp)

                     //   hubList.add(document.toObject(HubModel::class.java))

            } else {
                Log.d(TAG, "No such document")
            }

        }

I use this function in my ViewModel class to create a LiveData wrapped ArrayList:

val hubList =  MutableLiveData<ArrayList<HubModel>>()
    private val hubListDao = HubListDao()
    init {
        viewModelScope.launch {

            hubList.value = hubListDao.fetchHubList()
        }
    }

Thanks in advance!

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
Aman Kumar
  • 294
  • 8
  • 20
  • A snapshot listener can fire multiple times, while `await()` only waits for a single result. The two paradigms don't match. Why do you want to use a snapshot listener? – Frank van Puffelen Mar 30 '21 at 17:36
  • I want to use a smapshot listener to listen to changes which might occur in my database to update my recycler view – Aman Kumar Mar 30 '21 at 17:57
  • In that case you can't `await()` the results from the database, as `await` only expects data once. I'm no expert in `MutableLiveData`, but it seems you may be looking for `postValue`: https://stackoverflow.com/questions/44204978/how-to-update-livedata-of-a-viewmodel-from-background-service-and-update-ui – Frank van Puffelen Mar 30 '21 at 18:24
  • @FrankvanPuffelen I don't think postValue is of much help here. What I am actually looking is a way to perform the snapshotListening asynchronously. If I can't use await, then do I have to use something like GloabalScope(Dispatchers.IO) maybe? Then yes, postvalue will help I guess – Aman Kumar Mar 30 '21 at 18:32
  • Just a curiosity. Should I be using kotlin Flows? – Aman Kumar Mar 30 '21 at 18:55
  • Just a curiosity. Your question is called **"How to use Firestore database : addSnapshotListener using await() in Kotlin?"** and the accepted answer has nothing to do with that, right? The solution provided is using a LiveData object and not ".await()" (Kotlin coroutines) as required at all. – Alex Mamo Apr 01 '21 at 08:04
  • Yeah @AlexMamo But since you pointed out that it won't be possible, I am now forced to use Firebase UI since I am not willing to change my code that much according to the accepted answer. But I have marked it as accepted for someone's future use who would have written similar code. – Aman Kumar Apr 01 '21 at 12:25
  • You say *"I am now forced to use Firebase-UI"*, fair enough, as I explained in my below answer, right? And not the LiveData object, as in the accepted answer. Accepting an answer you don't even use it, might also confuse feature visitors. – Alex Mamo Apr 02 '21 at 09:54

2 Answers2

1

You don't need addSnapshotListener, just use get:

hubsListCollection.get().await()

In order to observe changes in your collection you can extend LiveData:

class CafeLiveData(
    private val documentReference: DocumentReference
) : LiveData<Cafe>(), EventListener<DocumentSnapshot> {

    private var snapshotListener: ListenerRegistration? = null

    override fun onActive() {
        super.onActive()
        snapshotListener = documentReference.addSnapshotListener(this)
    }

    override fun onInactive() {
        super.onInactive()
        snapshotListener?.remove()
    }

    override fun onEvent(result: DocumentSnapshot?, error: FirebaseFirestoreException?) {
        val item = result?.let { document ->
            document.toObject(Cafe::class.java)
        }
        value = item!!
    }
}

And expose it from your view model:

fun getCafe(id: String): LiveData<Cafe> {
    val query = Firebase.firestore.document("cafe/$id")
    return CafeLiveData(query)
}
sdex
  • 3,169
  • 15
  • 28
  • The problem with this approach is that if changes occur in my database, then I have to restart my activity so that the changes are loaded. I want the recycler view to get updated as soon as the changes occur in the database. – Aman Kumar Mar 30 '21 at 18:08
  • 1
    @ProfessorofStupidity I have added an example of custom LiveData implementation to observe changes. – sdex Mar 31 '21 at 07:01
1

As @FrankvanPuffelen already mentioned in his comment, there is no way you can use ".await()" along with "addSnapshotListener()", as both are two totally different concepts. One is used to get data only once, while the second one is used to listen to real-time updates. This means that you can receive a continuous flow of data from the reference you are listening to.

Please notice that ".await()" is used in Kotlin with suspend functions. This means that when you call ".await()", you start a separate coroutine, which is a different thread that can work in parallel with other coroutines if needed. This is called async programming because ".await()" starts the coroutine execution and waits for its finish. In other words, you can use ".await()" on a deferred value to get its eventual result, if no Exception is thrown. Unfortunately, this mechanism doesn't work with real-time updates.

When it comes to Firestore, you can call ".await()" on a DocumentReference object, on a Query object, or on a CollectionReference object, which is actually a Query without filters. This means that you are waiting for the result/results to be available. So you can get a document or multiple documents from such calls. However, the following call:

hubsListCollection.addSnapshotListener().await()

Won't work, as "addSnapshotListener()" method returns a ListenerRegistration object.

I want to use a snapshot listener to listen to changes that might occur in my database to update my RecyclerView

In this case, you should consider using a library called Firebase-UI for Android. In this case, all the heavy work will be done behind the scenes. So there is no need for any coroutine or ".await()" calls, everything is synched in real-time.

If you don't want to use either Kotlin Coroutines, nor Firebase-UI Library, you can use LiveData. A concrete example can be seen in my following repo:

Where you can subclass LiveData class and implement EventListener the interface.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Thanks! But I am trying to avoid Firebase UI, since I am trying to implement my app using MVVM architecture and they can't go together (your answer only, XD https://stackoverflow.com/q/66704121/12270082 ). Lately, I am reading about Flows which on the documentation is specifically designed for tasks like getting real-time updates from databases. Should I use that in my fetchHubList method? – Aman Kumar Mar 31 '21 at 11:54
  • 1
    Yes, it's one or the other. If you want to go ahead with MVVM, then the functionality of the library should be implemented somehow by yourself. Regarding Flow, [here](https://medium.com/firebase-tips-tricks/how-to-use-kotlin-flows-with-firestore-6c7ee9ae12f3) is a good resource about that. – Alex Mamo Mar 31 '21 at 11:59