Search code examples
androidandroid-lifecycleandroid-architecture-componentsandroid-livedataandroid-mvvm

Let every Observer only receive *new* LiveData upon subscribing/observing


Whenever you call .observe() on LiveData, the Observer receives the last value of that LiveData. This may be useful in some cases, but not in mine.

  1. Whenever I call .observe(), I want the Observer to receive only future LiveData changes, but not the value it holds when .observe() is called.

  2. I may have more than one Observer for a LiveData instance. I want them all to receive LiveData updates when they happen.

  3. I want each LiveData update to be consumed only once by each Observer. I think is just a re-phrasing of the first requirement, but my head is spinning already and I'm not sure about it.


While googling this problem, I came upon two common approaches:

  1. Wrap the data in an LiveData<SingleEvent<Data>> and check in this SingleEvent class if it was already consumed.

  2. Extend MediatorLiveData and use a look-up-map if the Observer already got the Event

Examples for these approaches can be found here: https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#gistcomment-2783677 https://gist.github.com/hadilq/f095120348a6a14251a02aca329f1845#file-liveevent-kt https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt

Unfortunately none of these examples solves all my requirements. Most of the time, the problem is that any new Observer still receives the last LiveData value upon subscribing. That means that a Snackbar which was already shown is displayed again and again whenever the user navigates between screens.


To give you some insights what I am talking about / what I am coding about:

I am following the LiveData MVVM design of the Android Architecture Componentns:

  • 2 ListFragment are showing a list of entries.
  • They are using 2 instances of the same ViewModel class to observe UI-related LiveData.
  • The user can delete an entry in such a ListFragment. The deletion is done by the ViewModel calling Repository.delete()
  • The ViewModel observes the Repository for RepositoryEvents.

So when the deletion is done, the Repository informs the ViewModel about it and the ViewModel inform the ListFragment about it.

Now, when the user switches to the second ListFragment the following happens:

  • The second Fragment gets created and calls .observe() on its ViewModel
  • The ViewModel gets created and calls .observe() on the Repository

  • The Repository sends its current RepositoryEvent to the ViewModel

  • The ViewModel send the according UI Event to the Fragment
  • The Fragment shows a confirmation Snackbar for a deletion that happened somewhere else.

Heres some simplified code:

Fragment:

viewModel.dataEvents.observe(viewLifecycleOwner, Observer { showSnackbar() })
viewModel.deleteEntry()

ViewModel:

val dataEvents: LiveData<EntryListEvent> = Transformations.switchMap(repository.events, ::handleRepoEvent)
fun deleteEntry() = repository.deleteEntry()
private fun handleRepoEvent(event: RepositoryEvent): LiveData<EntryListEvent> {
    // convert the repository event to an UI event
}

Repository:

private val _events = MutableLiveData<RepositoryEvent>()
val events: LiveData<RepositoryEvent>
    get() = _events

fun deleteEntry() {
    // delete it from database
    _events.postValue(RepositoryEvent.OnDeleteSuccess)
}

Solution

  • UPDATE 2021:

    Using the coroutines library and Flow it is now very easy to achieve this by implementing Channels:

    MainActivity

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import androidx.lifecycle.ViewModelProvider
    import androidx.lifecycle.lifecycleScope
    import com.google.android.material.snackbar.Snackbar
    import com.plcoding.kotlinchannels.databinding.ActivityMainBinding
    import kotlinx.coroutines.flow.collect
    
    class MainActivity : AppCompatActivity() {
    
        private lateinit var viewModel: MainViewModel
    
        private lateinit var binding: ActivityMainBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    
            binding.btnShowSnackbar.setOnClickListener {
                viewModel.triggerEvent()
            }
    
            lifecycleScope.launchWhenStarted {
                viewModel.eventFlow.collect { event ->
                    when(event) {
                        is MainViewModel.MyEvent.ErrorEvent -> {
                            Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
                        }
                    }
                }
            }
    
        }
    }
    

    MainViewModel

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.channels.consumeEach
    import kotlinx.coroutines.flow.receiveAsFlow
    import kotlinx.coroutines.launch
    
    class MainViewModel : ViewModel() {
    
        sealed class MyEvent {
            data class ErrorEvent(val message: String): MyEvent()
        }
    
        private val eventChannel = Channel<MyEvent>()
        val eventFlow = eventChannel.receiveAsFlow()
    
        fun triggerEvent() = viewModelScope.launch {
            eventChannel.send(MyEvent.ErrorEvent("This is an error"))
        }
    }