Passing MutableLiveData between functions

Viewed 970

I have a function 'A' in a ViewModel that retrieves data from firebase and I assign the value to a MutableLiveData<Int> (all of this is wrapped in onSuccessListener)and return it. This function is called from another function 'B' in the same ViewModel. But when I try to return MutableLiveData<Int> from 'A', it is returned as 0 (the default value). But if I assign value for the MutableLiveData<Int> outside the onSuccessListener, then the value is being returned.

Code:

val num = MutableLiveData<Int>().default(0)

private fun A():Int {
   FirebaseOperation
     .addOnSuccessListener{  //it:DocumentSnapshot!
         num.value = it.num
     }
   return num.value.toInt() // outside onsuccesslistener, default value 0 is being returned
}

private fun B() {
   val num2 = A()
}

Update: After learning about the firebase callback hell, I've switched to kotlin coroutine for firebase (implementing org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1 as a dependancy) Updated Code:

private suspend fun A():DocumentSnapshot? {
     return Firebase.firestore.collection("collection").documet("document").get().await()
    }

private suspend fun B(): Int{
    val data = A()
    val user = data.toObject<User>()
    val num = user.num
    return num
}

But the main thread freezes and the app crashes with Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 9.)

6 Answers

If you use kotlinx.coroutines, you can use suspendCoroutine.

private suspend fun A(): Int = suspendCoroutine { cont ->
    FirebaseOperation.addOnSuccessListener{  // it: DocumentSnapshot!
        cont.resume(it.num)
    }
}

private suspend fun B() {
   val num2 = A()
}

The retrieval of data from firebase happens asynchronously or in other words the addOnSuccessListener callback is only invoked when the data is retrieved from the firebase. So the livedata is only updated when the callback is invoked. when you call A() from B(), it returns 0 because it does not wait for the livedata to be updated (callback to be invoked) and just returns the default value.

When you simply update the livedata value outside the addOnSuccessListener, it is updated synchronously and hence you get the updated result

The main purpose of LiveData is to be updated after asynchronous work and to be observed. In function A() I see that you only add SuccessListener but no operation is executed, so SuccessListener is not invoked. In function B() you just retrieve a reference to MutableLiveData but don't observe it, so it always contains default value. Usually LiveData objects are observed in Activity or Fragment:

num.observe(lifecycleOwner, androidx.lifecycle.Observer {
    // here you retrieve changes of your "num" variable as parameter "it"
})

where lifecycleOwner - usually activity or fragment.

A way to get the value in function B without using coroutines and without observing the livedata by a lifecycle (which btw you can't do as you don't have a lifecycle owner in viewmodel and you should refrain from passing any to it), is to use a MediatorLiveData.

The main purpose of mediator live data is that depends on other livedata for it's value and it can observe other livedata without a lifecycle.

So you can create a MediatorLiveData inside the function B, then return the livedata from function A and then observe it using the MediatorLiveData in function B. That way, when the value will get updated in A, you will be notified in B.

You can read more about MediatorLiveData here: https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData

You could have flow or you can directly feed your VM.

class YourViewModel:ViewModel(){

val num = MutableLiveData<Int>().default(0)

   private suspend fun A():DocumentSnapshot? {
     return Firebase.firestore.collection("collection").documet("document").get().await()
   }

   fun onSomeButtonClick(){
       viewModelScope.launch(Main){
          val num =  A()?.toObject<User>().num ?: return
          num.value = num
       }
   } 

}

or flow with asLiveData extension.

val num:LiveData<Int> = flow{
   val num =  A()?.toObject<User>()?.num ?: 0
   emit(num)
}.asLiveData()

In your UI, you can observe and get id.

I somehow managed to merge both the functions and I've nested all the DB callbacks now. I don't think that's good for code readability though. I'll be trying out other alternatives meanwhile.

Related