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

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.

1 answer

  • answered 2018-08-09 09:06 TheTechWolf

    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.