How to create LiveData which emits a single event and notifies only last subscribed observer?

14,517

In the end, I found a workaround for this problem. I had to move away from the live data that emits a single event since it couldn't behave the way I needed it to behave.

Instead of this, I used simple mutable live data which emits an event object which wraps a data as in the last paragraph of this article by Jose Alcérreca.

I'm showing fragments in a view pager so I have only one visible fragment at the time.

So my view model looks like this:

class ActionViewModel : ViewModel() {
  private val onCreateLiveData: MutableLiveData<Event<String>> = MutableLiveData()

  fun observeOnCreateEvent(): LiveData<Event<String>> = onCreateLiveData

  fun onCreateCollectionClick(message: String) {
    this.onCreateLiveData.value = Event(message)
  }
}

Event wrapper class implementation looks like this:

/*Used as a wrapper for data that is exposed via a LiveData that represents an 
 event.*/

open class Event<out T>(private val content: T) {

  var hasBeenHandled = false
    private set // Allow external read but not write

  /**
   * Returns the content and prevents its use again.
  */
  fun getContentIfNotHandled(): T? {
    return if (hasBeenHandled) {
      null
    } else {
      hasBeenHandled = true
      content
    }
  }

  /**
    * Returns the content, even if it's already been handled.
  */
  fun peekContent(): T = content
}

In fragments now we can observe events like this:

override fun onActivityCreated(savedInstanceState: Bundle?) {
   super.onActivityCreated(savedInstanceState)

   actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionViewModel::class.java)
   actionViewModel.observeOnCreateEvent()
       .observe(this, Observer {
         it?.takeIf { userVisibleHint }?.getContentIfNotHandled()?.let {
           //DO what ever is needed
         }
       })
}

Fragment userVisibleHint property will return true if the fragment is currently visible to the user. Since we are only showing one fragment at the time this works for us. This means that the fragment will only access the event data if it is visible.

Also, implementation of the Event wrapper allows only one read of the value, so that every next time Observer gets this event, its value will be null and we'll ignore it.

Conclusion: This way we are simulating a single event live data which notifies only last subscribed observer.

Share:
14,517
TheTechWolf
Author by

TheTechWolf

Updated on June 17, 2022

Comments

  • TheTechWolf
    TheTechWolf about 2 years

    I created live data which emits a single event as in this example.

    My question is next: How to notify only last subscribed observer when the value in the LiveData changes?

    What comes to my mind is to store observers in the linked list in SingleLiveData class and then to call super.observe only if a passed observer is the same as the last element of the list.

    I'm not sure if this is the best approach.

    I want to use this mechanism to propagate FAB click events from activity to the fragments which are shown inside of the ViewPager. Fragments are dynamically added to view pager adapter, so let's say that we know the order of the fragments.

  • Hoang Nguyen Huu
    Hoang Nguyen Huu over 4 years
    Simple and useful! Thanks.
  • ror
    ror over 4 years
    To be precise it does not solve "How to notify only last subscribed observer". First in line will get notified. It's just your setup that helps: likely your live count of fragments in pager is 1, so all not visible fragments are stopped thus cannot compete for the event.
  • JBest
    JBest over 2 years
    This answer solved my similar problem, thanks!