16

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.

Mycoola
  • 1,135
  • 1
  • 8
  • 29
TheTechWolf
  • 2,084
  • 2
  • 19
  • 24
  • so probably `MediatorLifeData` (or `Transformations#switchMap`) is what you could use for such case – pskink Aug 09 '18 at 07:29
  • Hm, I'm not sure how this would solve my problem. I need a way to notify last or all subscribed observers and after it to set the value to null. Could you elaborate a little bit more? – TheTechWolf Aug 09 '18 at 08:04
  • 1
    `"last or all subscribed observers"` so last or all? – pskink Aug 09 '18 at 08:08
  • Last would be preferable, but I could also work with it if all get update ;) – TheTechWolf Aug 09 '18 at 08:10
  • if last than ok maybe `MutableLifeData` would be better with overriden `observe` method where you call `removeObservers` method followed by `super.observe` - that way you are sure that at most only one observer is active – pskink Aug 09 '18 at 08:10
  • That is a good idea. Then I'll just have to find a way to add this observer again when one of the fragments on the stack becomes visible. – TheTechWolf Aug 09 '18 at 08:14

5 Answers5

15

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.

TheTechWolf
  • 2,084
  • 2
  • 19
  • 24
  • Simple and useful! Thanks. – Hoang Nguyen Huu Oct 31 '19 at 04:31
  • 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. – ror Feb 26 '20 at 08:39
  • This answer solved my similar problem, thanks! – jbdev Oct 25 '21 at 20:11
  • I don't really like "Jose Alcérreca"'s solution. This is like you will keep listening to something that you only needed just once, which makes no sense. This is not a solution this is just crappy workaround that simulates it the way you want. – Farid Nov 08 '22 at 08:02
2

If you're using Kotlin, you can replace LiveData with Flow. StateFlow can be used to replace regular LiveData, while SharedFlow can be used for stateless events. It will also provide you null safety and all the operators and configurations that come with Flow.

The migration is described here among other places. Here's a basic example:

ViewModel:

interface MyViewModel {
    val myData: StateFlow<MyData>
    val myEvents: SharedFlow<MyEvent>
}

class MyViewModelImpl: MyViewModel {
    override val myData = MutableStateFlow(MyData())
    override val myEvents = MutableSharedFlow<MyEvent>(replay = 0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST)

    /*
     * Do stuff 
     */
}

Activity:

lifecycleScope.launch {
    myData.collect {
        // handle stateful data    
    }
}

lifecycleScope.launch {
    myEvents.collect {
        // handle stateless events    
    }
}

Note that lifecycleScope requires the appropriate ktx dependency.

Herer's some more reading about Flow in Android.

Sir Codesalot
  • 7,045
  • 2
  • 50
  • 56
1

I found solution for me in LD extension:

fun <T> LiveData<T>.observeAsEvent(owner: LifecycleOwner, observer: Observer<in T>) {
    var previousKey: Any? = value?: NULL
    observe(owner) { value ->
        if (previousKey == NULL || previousKey != value) {
            previousKey = value
            observer.onChanged(value)
        }
    }
}

private const val NULL = "NULL"

Usage for this:

viewModel.resultLiveData.observeAsEvent(viewLifecycleOwner) { 
     ...
}
0

I crafted a solution, feel free to take a look https://github.com/ueen/LiveEvent

ueen
  • 692
  • 3
  • 9
  • 21
0

I've created a library to handle the most common cases that we might encounter while working with event-driven data scenarios

https://github.com/javaherisaber/LiveX

It contains the following types of classes:

LiveData LiveEvent OneShotLiveEvent SingleLiveEvent
Multiple observers can register, all of them receive the event based on lifecycle Multiple observers can register, each one receive the event only once Only one observer can register and receive the event only once Multiple observers can register, only the first one receive the event
Mahdi Javaheri
  • 1,080
  • 13
  • 25