Skip to content

Commit

Permalink
Refactor LiveEvent
Browse files Browse the repository at this point in the history
It now uses more composable approach with LiveData as data member instead of as base class

- Much more clear implementation semantics
- Clear & concise API
- Added support for removeObserver()
- Easier to test
  • Loading branch information
gujjwal00 committed Dec 26, 2023
1 parent b64b781 commit 2c18361
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 93 deletions.
71 changes: 39 additions & 32 deletions app/src/androidTest/java/com/gaurav/avnc/viewmodel/LiveEventTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

package com.gaurav.avnc.viewmodel

import androidx.test.espresso.Espresso.onIdle
import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.gaurav.avnc.instrumentation
import com.gaurav.avnc.pollingAssert
import com.gaurav.avnc.runOnMainSync
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -23,21 +24,21 @@ class LiveEventTest {
* This is the main difference between [LiveEvent] & [androidx.lifecycle.LiveData].
*/
@Test
fun futureObserversShouldNotBeNotified() {
val testEvent = LiveEvent<Boolean>()
fun exactlyOneObserverShouldBeNotified() {
val testEvent = LiveEvent<Any>()
var observer1Notified = false
var observer2Notified = false
var futureObserverNotified = false

instrumentation.runOnMainSync {
testEvent.observeForever { observer1Notified = true } // Observer 1
testEvent.observeForever { observer2Notified = true } // Observer 2
testEvent.fire(true)
testEvent.observeForever { futureObserverNotified = true } // Observer installed after event was fired
runOnMainSync {
testEvent.observeForever { observer1Notified = true }
testEvent.observeForever { observer2Notified = true }
testEvent.fire(Any())
testEvent.observeForever { futureObserverNotified = true }
}

Assert.assertTrue(observer1Notified)
Assert.assertTrue(observer2Notified)
Assert.assertFalse(observer2Notified)
Assert.assertFalse(futureObserverNotified)
}

Expand All @@ -48,39 +49,45 @@ class LiveEventTest {
*/
@Test
fun eventShouldNotBeLost() {
val testEvent = LiveEvent<Boolean>()
val testEvent = LiveEvent<Any>()
var observerNotified = false

instrumentation.runOnMainSync {
testEvent.fire(true)
runOnMainSync {
testEvent.fire(Any())
testEvent.observeForever { observerNotified = true }
}

Assert.assertTrue(observerNotified)
}

/**
* Working of [LiveEvent] relies on setValue() being called in response to fireAsync().
* This test verifies that assumption.
*/
@Test
fun checkSetValueIsCalledAfterFireAsync() {
val testValue = Any()
var setValueCalled = false
var setValueCalledWith: Any? = Any()

val testEvent = object : LiveEvent<Any>() {
override fun setValue(value: Any?) {
super.setValue(value)
setValueCalled = true
setValueCalledWith = value
}
fun testObserverRemoval() {
val testEvent = LiveEvent<Any>()
var notified = false

runOnMainSync {
val observer = Observer<Any> { notified = true }
testEvent.observeForever(observer)
testEvent.removeObserver(observer)
testEvent.fire(Any())
}

testEvent.fireAsync(testValue)
onIdle()
Assert.assertFalse(notified)
}

Assert.assertTrue(setValueCalled)
Assert.assertSame(testValue, setValueCalledWith)
@Test
fun testAsyncFiring() {
val testEvent = LiveEvent<Int>()
var observedValue = 0

runOnMainSync {
testEvent.observeForever { observedValue = it }
}

testEvent.fireAsync(1)
pollingAssert {
Assert.assertEquals(1, observedValue)
Assert.assertEquals(1, testEvent.value)
}
}
}
103 changes: 42 additions & 61 deletions app/src/main/java/com/gaurav/avnc/viewmodel/LiveEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

package com.gaurav.avnc.viewmodel

import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer

/**
Expand All @@ -18,91 +20,70 @@ import androidx.lifecycle.Observer
*
* Single-shot
* ===========
* When this event is fired, it will notify all active observers.
* If there is no active observer, it will wait for active observers,
* so that the event is not "lost". After notifying observers, it will
* be marked as 'handled' and any future observers will NOT be notified.
* When this event is fired, it will notify exactly one active observer.
* If there is no active observer, it will wait for one so that the event
* is not "lost".
*
* This is the main difference between this class & [LiveData]. [LiveData] will
* notify the future observers to bring them up-to date. This can happen during
* Activity restarts where old observers are detached and new ones are attached.
*
* This class is used for events which should be handled only once.
* e.g starting a fragment.
*
* Calling [removeObserver] on [LiveEvent] is NOT supported because we wrap
* the observer given to us in a custom observer, which is currently not
* exposed to callers (see [wrapObserver]).
* E.g. starting a fragment.
*/
open class LiveEvent<T> : LiveData<T>() {

/**
* Whether we are currently firing the event. Observers will be notified
* only when this is true.
*/
private var firing = false

/**
* Whether someone has handled the last event fired.
* This is used to implement the "queuing" behaviour:
*
* 1. When event is fired, set this to false
* 2. If observers are invoked, set this to true
* 3. In [onActive], if this is still false, re-fire
*/
private var handled = true
open class LiveEvent<T> {

private class WrappedData<T>(val data: T, var consumed: Boolean = false)
private class WrappedObserver<T>(private val observer: Observer<T>) : Observer<WrappedData<T>> {
override fun onChanged(value: WrappedData<T>) {
if (!value.consumed) {
value.consumed = true
observer.onChanged(value.data)
}
}
}

/**
* Fire this event with given value.
* MUST be called from Main thread.
*/
open fun fire(value: T?) = setValue(value)
private val liveData = MutableLiveData<WrappedData<T>>()
private val wrappedObservers = mutableMapOf<Observer<T>, WrappedObserver<T>>()

/**
* Same as [fire], but can be called from any thread.
* Peek current value of this event, irrespective of whether any observer has been notified.
*/
fun fireAsync(value: T?) = postValue(value)
val value get() = liveData.value?.data

/**
* Overridden to manage event state.
* Fire this event with given data.
* Must be called from main thread.
*/
override fun setValue(value: T?) {
firing = true
handled = false
super.setValue(value)
firing = false
@MainThread
fun fire(data: T) {
liveData.value = WrappedData(data)
}

/**
* Overridden to check for queued event.
* Asynchronous version of [fire].
* Can be called from any thread.
*/
override fun onActive() {
super.onActive()

if (!handled)
fire(value)
fun fireAsync(data: T) {
liveData.postValue(WrappedData(data))
}


override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, wrapObserver(observer))
@MainThread
fun observe(owner: LifecycleOwner, observer: Observer<T>) {
val wrapped = WrappedObserver(observer)
wrappedObservers[observer] = wrapped
liveData.observe(owner, wrapped)
}

override fun observeForever(observer: Observer<in T>) {
super.observeForever(wrapObserver(observer))
@MainThread
fun observeForever(observer: Observer<T>) {
val wrapped = WrappedObserver(observer)
wrappedObservers[observer] = wrapped
liveData.observeForever(wrapped)
}

/**
* Observer given to us is wrapped in another Observer
* which checks current state before invoking real observer.
*/
private fun <T> wrapObserver(real: Observer<in T>): Observer<T> {
return Observer {
if (firing) {
real.onChanged(it)
handled = true
}
}
@MainThread
fun removeObserver(observer: Observer<T>) {
wrappedObservers.remove(observer)?.let { liveData.removeObserver(it) }
}
}

0 comments on commit 2c18361

Please sign in to comment.