Observing LiveData from ViewModel

96,848

Solution 1

In this blog post by Google developer Jose Alcérreca it is recommended to use a transformation in this case (see the "LiveData in repositories" paragraph) because ViewModel shouldn't hold any reference related to View (Activity, Context etc) because it made it hard to test.

Solution 2

In ViewModel documentation

However ViewModel objects must never observe changes to lifecycle-aware observables, such as LiveData objects.

Another way is for the data to implement RxJava rather than LiveData, then it won't have the benefit of being lifecycle-aware.

In google sample of todo-mvvm-live-kotlin, it uses a callback without LiveData in ViewModel.

I am guessing if you want to comply with the whole idea of being lifecycle-ware, we need to move observation code in Activity/Fragment. Else, we can use callback or RxJava in ViewModel.

Another compromise is implement MediatorLiveData (or Transformations) and observe (put your logic here) in ViewModel. Notice MediatorLiveData observer won't trigger (same as Transformations) unless it's observed in Activity/Fragment. What we do is we put a blank observe in Activity/Fragment, where the real work is actually done in ViewModel.

// ViewModel
fun start(id : Long) : LiveData<User>? {
    val liveData = MediatorLiveData<User>()
    liveData.addSource(dataSource.getById(id), Observer {
        if (it != null) {
            // put your logic here
        }
    })
}

// Activity/Fragment
viewModel.start(id)?.observe(this, Observer {
    // blank observe here
})

PS: I read ViewModels and LiveData: Patterns + AntiPatterns which suggested that Transformations. I don't think it work unless the LiveData is observed (which probably require it to be done at Activity/Fragment).

Solution 3

I think you can use observeForever which does not require the lifecycle owner interface and you can observe results from the viewmodel

Solution 4

Use Flow

The guideline in docs is misunderstood

However ViewModel objects must never observe changes to lifecycle-aware observables, such as LiveData objects.

In this Github issue, he describes that the situations that be applied the above rule are that observed lifecycle-aware observables are hosted by another lifecycle scope. There is no problem that observes LiveData in ViewModel contains observed LiveData.

Use Flow

class MyViewModel : ViewModel() {
    private val myLiveData = MutableLiveData(1)

    init {
        viewModelScope.launch {
            myLiveData.asFlow().collect {
                // Do Something
            }
        }
    }
}

Use StateFlow

class MyViewModel : ViewModel() {
    private val myFlow = MutableStateFlow(1)
    private val myLiveData = myFlow.asLiveData(viewModelScope.coroutineContext)
}

PS

The asFlow makes a flow that makes LiveData activate at starting collect. I think the solution with MediatorLiveData or Transformations and attaching a dummy observer doesn't have differences using the Flow except for emitting value from LiveData is always observed in the ViewModel instance.

Solution 5

Use Kotlin coroutines with Architecture components.

You can use the liveData builder function to call a suspend function, serving the result as a LiveData object.

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

You can also emit multiple values from the block. Each emit() call suspends the execution of the block until the LiveData value is set on the main thread.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

In your gradle config, use androidx.lifecycle:lifecycle-livedata-ktx:2.2.0 or higher.

There is also an article about it.

Update: Also it's possible to change LiveData<YourData> in the Dao interface. You need to add the suspend keyword to the function:

@Query("SELECT * FROM the_table")
suspend fun getAll(): List<YourData>

and in the ViewModel you need to get it asynchronously like that:

viewModelScope.launch(Dispatchers.IO) {
    allData = dao.getAll()
    // It's also possible to sync other data here
}
Share:
96,848

Related videos on Youtube

Vuk Bibic
Author by

Vuk Bibic

Updated on July 08, 2022

Comments

  • Vuk Bibic
    Vuk Bibic almost 2 years

    I have a separate class in which I handle data fetching (specifically Firebase) and I usually return LiveData objects from it and update them asynchronously. Now I want to have the returned data stored in a ViewModel, but the problem is that in order to get said value, I need to observe the LiveData object returned from my data fetching class. The observe method required a LifecycleOwner object as the first parameter, but I obviously don't have that inside of my ViewModel and I know I am not supposed to keep a reference to the Activity/Fragment inside of the ViewModel. What should I do?

  • romaneso
    romaneso over 6 years
    have you managed to get Transformation working for you? My events aren't working
  • orbitbot
    orbitbot over 6 years
    Transformations on their own do not work, since whatever code you write in the transformation is only attached to run when some entity observes the transformation.
  • qbait
    qbait almost 6 years
    Did anything change in this regard? Or RX, callback or blank observe are only solutions?
  • Ehsan Mashhadi
    Ehsan Mashhadi over 5 years
    Any solution to get rid of these blank observes?
  • Yosef
    Yosef almost 5 years
    this seems the right answer to me especially that in the docs about ViewModel.onCleared() is said: "It is useful when ViewModel observes some data and you need to clear this subscription to prevent a leak of this ViewModel."
  • Boken
    Boken over 4 years
    Sorry but Cannot invoke observeForever on a background thread
  • Kirill Starostin
    Kirill Starostin over 4 years
    That seems quite legitimate. Though one has to save observers in the viewModel fields and unsubscribe at onCleared. As to the background thread - observe from main thread, that's it.
  • rmirabelle
    rmirabelle over 4 years
    @Boken You can force observeForever to be invoked from main via GlobalScope.launch(Dispatchers.Main) { myvm.observeForever() }
  • Machado
    Machado almost 4 years
    Maybe using Flow (mLiveData.asFlow()) or observeForever.
  • Andrew
    Andrew almost 4 years
    I don't know why this is the recommended answer, it has nothing to do with the question. 2 years later, and we still don't know how to observe repository data changes in our viewmodel.
  • adek111
    adek111 over 3 years
    Flow solution seems to work if you don't want to have/you don't need any observer logic in Fragment
  • Oguzhan
    Oguzhan about 3 years
    After few hours of research. I suppose the only good way to do this is using a blank observe in the view component because observeForever may cause problems when the view (that the observable supposed to be attached) is destroyed. This is what I could find, I am still on the question. I will update if I find a more convenient answer .
  • Archid
    Archid about 3 years
    @Andrew Hi Andrew, I am curious. Why not observe data in the fragment as viewmodel.repository.liveModelData.observe {this, }
  • Andrew
    Andrew about 3 years
    That doesn't answer the question? What if you need a specific value inside your viewmodel to do other operations and then serve it to the fragment??