Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An Observer for Events, simplifying the pattern of checking if the Event's content has already been handled.
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
@kroegerama

This comment has been minimized.

Copy link

@kroegerama kroegerama commented Jun 18, 2018

You can make lines 9..11 a one liner:

event?.getContentIfNotHandled()?.let(onEventUnhandledContent)
@AdamSHurwitz

This comment has been minimized.

Copy link

@AdamSHurwitz AdamSHurwitz commented Sep 23, 2018

Thank you! This solved my need for handling click events with LiveData. One small tweak: I changed the name of the method getContentIfNotHandled to getEventIfNotHandled since it is more generic.

Here is a sample implementation.

// someOneTimeEffect is wrapped in an 'Event'
someViewModel.someOneTimeEffect.observe(viewLifecycleOwner, EventObserver { it ->
    // Do something here with 'it'.
})
@AlexBacich

This comment has been minimized.

Copy link

@AlexBacich AlexBacich commented Oct 30, 2018

How can I use this EventObserver? Can you give an example. Probably topic is still too hard for me, but the original article was clear: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

@eyeahs

This comment has been minimized.

Copy link

@eyeahs eyeahs commented Nov 1, 2018

inline fun <T> LiveData<Event<T>>.observeEvent(owner: LifecycleOwner, crossinline onEventUnhandledContent: (T) -> Unit) {
    observe(owner, Observer { it?.getContentIfNotHandled()?.let(onEventUnhandledContent) })
}
@WebTiger89

This comment has been minimized.

Copy link

@WebTiger89 WebTiger89 commented Nov 28, 2018

Am I wrong or can't EventObserver handle null events? i.e. new Event<>(null). I'm not in Kotlin but when I got it right it?.getContentIfNotHandled()?.let(onEventUnhandledContent)

onEventUnhandledContent

is only called if event != null and the return value != null so with new Event<>(null)

onEventUnhandledContent

is never called?!

@dri94

This comment has been minimized.

Copy link

@dri94 dri94 commented Feb 26, 2019

@WebTiger89 yes you are correct, not completely ideal

@ildar2

This comment has been minimized.

Copy link

@ildar2 ildar2 commented Mar 13, 2019

@WebTiger89 you can use Null-objects for empty events, e.g. class VoidEvent or MyObject.EMPTY

@Nghicv

This comment has been minimized.

Copy link

@Nghicv Nghicv commented Apr 28, 2019

Thank you

@OssamaDroid

This comment has been minimized.

Copy link

@OssamaDroid OssamaDroid commented May 3, 2019

@AlexBacich You have to replace the Observer instantiation by the EventObserver one.
If we go back to the article you've mentioned (https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150) where it sounded pretty clear to you, we'll have something like this:
myViewModel.navigateToDetails.observe(this, EventObserver<T> { value -> startActivity(DetailsActivity...) } })
This is simply added to avoid the getContentIfNotHandled() checking every time :)

@jhamin0511

This comment has been minimized.

Copy link

@jhamin0511 jhamin0511 commented Nov 12, 2019

[before]

openTaskEvent.observe(this@TasksActivity, Observer<Event<String>> { event ->
                event.getContentIfNotHandled()?.let {
                    openTaskDetails(it)
                }
            })

[after]
openTaskEvent.observe(this@TasksActivity, EventObserver(::openTaskDetails))

@Kisty

This comment has been minimized.

Copy link

@Kisty Kisty commented Dec 4, 2019

@WebTiger89 @ildar2 Couldn't you use Unit for an empty event?

@erfanegtfi

This comment has been minimized.

Copy link

@erfanegtfi erfanegtfi commented Jan 14, 2020

Hi, can someone convert above code to java?

@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Jan 24, 2020

I recommend learning enough Kotlin that you can convert it back to Java if required.

@nathanmkaya

This comment has been minimized.

Copy link

@nathanmkaya nathanmkaya commented Feb 19, 2020

@erfanegtfi This is what I have for the java implementation

import androidx.lifecycle.Observer;

public class EventObserver<T> implements Observer<Event<T>> {

    private Listener<T> listener;

    public EventObserver(Listener<T> listener) {
        this.listener = listener;
    }

    @Override
    public void onChanged(Event<T> event) {
        if (event != null){
            T content = event.getContentIfNotHandled();
            if (content != null){
                listener.onEventUnhandledContent(content);
            }
        }
    }

    public interface Listener<T> {
        void onEventUnhandledContent(T t);
    }
}
@erfanegtfi

This comment has been minimized.

Copy link

@erfanegtfi erfanegtfi commented Feb 29, 2020

@erfanegtfi This is what I have for the java implementation

import androidx.lifecycle.Observer;

public class EventObserver<T> implements Observer<Event<T>> {

    private Listener<T> listener;

    public EventObserver(Listener<T> listener) {
        this.listener = listener;
    }

    @Override
    public void onChanged(Event<T> event) {
        if (event != null){
            T content = event.getContentIfNotHandled();
            if (content != null){
                listener.onEventUnhandledContent(content);
            }
        }
    }

    public interface Listener<T> {
        void onEventUnhandledContent(T t);
    }
}

Thank you

@TylerMcCraw

This comment has been minimized.

Copy link

@TylerMcCraw TylerMcCraw commented Mar 9, 2020

In order to help those who would like an example of using Events and a LiveData extension function, here's what has worked for me:

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

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

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

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

class VoidEvent {
    private var hasBeenHandled = false

    fun hasBeenHandled(): Boolean = if (hasBeenHandled) {
        true
    } else {
        hasBeenHandled = true
        false
    }
}

fun <T> LiveData<out Event<T>>.observeEvent(owner: LifecycleOwner, onEventUnhandled: (T) -> Unit) {
    observe(owner, Observer { it?.getContentIfNotHandled()?.let(onEventUnhandled) })
}

fun LiveData<out VoidEvent>.observeEvent(owner: LifecycleOwner, onEventUnhandled: () -> Unit) {
    observe(owner, Observer { if (!it.hasBeenHandled()) onEventUnhandled() })
}

class MyViewModel() : ViewModel() {
    val openScreenThatNeedsSomething: LiveData<Event<String>>
        get() = _openScreenThatNeedsSomething
    private val _openScreenThatNeedsSomething = MutableLiveData<Event<String>>()

    val openScreenThatNeedsNothing: LiveData<VoidEvent>
        get() = _openScreenThatNeedsNothing
    private val _openScreenThatNeedsNothing = MutableLiveData<VoidEvent>()
    
    fun onOpenScreenThatNeedsSomethingClicked() {
       _openScreenThatNeedsSomething.value = Event("Hello, World")
    }

    fun onOpenScreenThatNeedsNothingClicked() {
       _openScreenThatNeedsNothing.value = VoidEvent()
    }
}

class MyFragment() : Fragment() {

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      buttonSomething.setOnClickListener { viewModel.onOpenScreenThatNeedsSomethingClicked() }
      buttonNothing.setOnClickListener { viewModel.onOpenScreenThatNeedsNothingClicked() }

      viewModel.openScreenThatNeedsSomething.observeEvent(viewLifecycleOwner) { myString ->
         // open a second fragment using myString
      }
      viewModel.openScreenThatNeedsNothing.observeEvent(viewLifecycleOwner) {
         // open a third fragment that doesn't need anything
      }
   }
}
@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Mar 10, 2020

The unnecessary ping-pong from MVP implemented through event bus sticky events. Yet no one once thought "wow that's a lot of plumbing to write something trivial". Even writing this code in the Fragment directly was cleaner.

@TylerMcCraw

This comment has been minimized.

Copy link

@TylerMcCraw TylerMcCraw commented Mar 10, 2020

Seems like a lot of plumbing at first glance, but we're talking about a one-time 30 extra lines of code here to have properly implemented single event handling.
From someone who has been using MVP for 5+ years, I have written way more for nuanced architectural details with MVP.
Also, writing all of your event handling in your fragments was not cleaner by any means.

Having all of this said, I don't think this is the proper place for discussing what is the better architecture pattern for Android development. These discussions tend to get a little lengthy and we're in a gist lol
You know what, I actually think it's totally fine to discuss this here. At some point we'll all collectively account for all of these nuanced architectural details and decide if it was worth it. And this is pointing out a detail that may lead us to decide it wasn't worth it.

@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Mar 10, 2020

Don't take it personally, you're not the inventor of this new fad.

    private val _openScreenThatNeedsNothing = MutableLiveData<VoidEvent>()
    val openScreenThatNeedsNothing: LiveData<VoidEvent>
        get() = _openScreenThatNeedsNothing

    fun onOpenScreenThatNeedsNothingClicked() {
       _openScreenThatNeedsNothing.value = VoidEvent()
    }

This in the era of either MVP or handling-in-fragment looked like this:

view.openScreenThatNeedsNothing()

And the two snippets were almost equivalent.

This is what I'm referring to: the per-function-call-create-LiveData-with-Mutable-backing-field cruft that represents 1 synchronous method call. I was experimenting with a similar pattern back in 2016 but there's just so much overhead it's not worth it, and now we have it here as some kind of "jetpack industry standard". 🤔

@aminography

This comment has been minimized.

Copy link

@aminography aminography commented Apr 8, 2020

I've implemented this concept a bit neater with thread-safety in Kotlin. It might be helpful.

import java.util.concurrent.atomic.AtomicBoolean

/**
 * @author aminography
 */
class OneTimeEvent<T>(
    private val value: T
) {

    private val isConsumed = AtomicBoolean(false)

    internal fun getValue(): T? =
        if (isConsumed.compareAndSet(false, true)) value
        else null
}

fun <T> T.toOneTimeEvent() =
    OneTimeEvent(this)

fun <T> OneTimeEvent<T>.consume(block: (T) -> Unit): T? =
    getValue()?.also(block)

First, when you want to post a value on LiveData, use toOneTimeEvent() extension function to wrap it with the OneTimeEvent:

yourLiveData.postValue(yourObject.toOneTimeEvent())

Second, when you are observing on the LiveData, use consume { } function on the delivered value to gain the feature of OneTimeEvent. You'll be sure that the block of consume { } will be executed only once, among all Observers.

yourLiveData.observe(this, Observer {
            it.consume { yourObject->
                // TODO: do whatever with 'yourObject'
            }
        })
@IgorGanapolsky

This comment has been minimized.

Copy link

@IgorGanapolsky IgorGanapolsky commented May 25, 2020

crossinline

This doesn't seem readable. Certainly not to beginners.

@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented May 25, 2020

There is nothing wrong with crossinline, although I must admit it was one of those things I've asked like 4 people for a month before I understood what it does, having seen code written by Jake Wharton where he uses it to wrap a multi-method abstract class to receive optional implementations through the constructor, and invoking said lambdas from overridden methods of an inline anonymous object implementation.

Just to actually mention an approach that might help resolve some issues, see https://medium.com/@Zhuinden/simplifying-jetpack-navigation-between-top-level-destinations-using-dagger-hilt-3d918721d91e

@happytuna75

This comment has been minimized.

Copy link

@happytuna75 happytuna75 commented Jul 4, 2020

Thanks @aminography, very neat implementation, it's maybe not for total beginners but it's no rockets science for who's has some knowledge of thread safety... and the boilerplate is quite reduced.
I've a question though, I'm not an expert in Kotlin, why did you implement the following as an extension function and not directly as pasrt of the OneTimeEvent class?

fun <T> OneTimeEvent<T>.consume(block: (T) -> Unit): T? =
    getValue()?.also(block)
@aminography

This comment has been minimized.

Copy link

@aminography aminography commented Jul 6, 2020

Thanks @aminography, very neat implementation, it's maybe not for total beginners but it's no rockets science for who's has some knowledge of thread safety... and the boilerplate is quite reduced.
I've a question though, I'm not an expert in Kotlin, why did you implement the following as an extension function and not directly as pasrt of the OneTimeEvent class?

fun <T> OneTimeEvent<T>.consume(block: (T) -> Unit): T? =
    getValue()?.also(block)

You're welcome :)
I'd written the extension functions in a separate file before I wrote them here. So, you can put the consume into the OneTimeEvent class, no problem.

@AdamSHurwitz

This comment has been minimized.

Copy link

@AdamSHurwitz AdamSHurwitz commented Jul 16, 2020

For those working with Kotlin Flow I have created an extension function, onEachEvent, using the Event wrapper above. You may find the full explanation and code here.

OnEachEvent.kt

/**
 * Returns a flow which performs the given [action] on each value of the original flow's [Event].
 */
public fun <T> Flow<Event<T?>>.onEachEvent(action: suspend (T) -> Unit): Flow<T> = transform { value ->
    value.getContentIfNotHandled()?.let {
        action(it)
        return@transform emit(it)
    }
}
@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Jul 16, 2020

I think if you're using a Flow that is not a StateFlow, you don't need Event wrappers at all, as your events are not replayed on subscription anyway.

@AdamSHurwitz

This comment has been minimized.

Copy link

@AdamSHurwitz AdamSHurwitz commented Jul 17, 2020

@Zhuinden, In the use case in Coinverse a MutableStateFlow is used in the Fragment in order to emit intents in the Model-View-Intent pattern. In the ViewModel, the intent is observed as a Flow which is non-mutable so that the event is unidirectional.

I observed when triggering the selectContent intent that opens content in the feed, and navigating to a different screen, that upon returning, the intent is emitting a second time reopening the past content selected. After refactoring with the Event pattern this has resolved the issue.

This is good to know for Flow.

as your events are not replayed on subscription (with Flow)

When implementing MutableStateFlow to update intents, the Event pattern has utility as Roman Elizarov also summarizes.

If you use StateFlow to deliver events to your views (as opposed to the delivering a state), you must use an event wrapper class just as suggested by the article you've linked to: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
With the event wrapper you can ensure that every event is handled at most once.

The full code is under the refactor/mvi branch which will be merged to root.

FeedView.kt

@ExperimentalCoroutinesApi
interface FeedView {
    /**
     * Intent to select content from the feed
     *
     * @return A flow that emits the content to select from the feed
     */
    fun selectContent(): Flow<Event<SelectContent?>>
}

FeedFragment.kt

// MutableStateFlow passed into Adapter
val selectContent: MutableStateFlow<Event<SelectContent?>> = MutableStateFlow(Event(null)),

FeedAdapter.kt

private fun createOnClickListener(content: Content, position: Int) = OnClickListener { view ->
        when (view.id) {
            preview, contentTypeLogo -> intent.selectContent.value =
                    Event(SelectContent(content, position))
        }
    }

FeedViewModel.kt

fun bindIntents(view: FeedView) {
    view.selectContent().onEachEvent { selectContent ->
        selectContent(selectContent)
    }.launchIn(coroutineScope)
}

private fun selectContent(selectContent: SelectContent) {
    when (selectContent.content.contentType) {
        ARTICLE -> repository.getAudiocast(selectContent).onEach { resource ->
            when (resource.status) {
                LOADING -> state.value = OpenContent(...)
                SUCCESS -> state.value = OpenContent(...)
                Status.ERROR -> state.value = OpenContent(...)
            }
        }.launchIn(coroutineScope)
        YOUTUBE -> state.value = OpenContent(...)
        NONE -> throw IllegalArgumentException("contentType expected, contentType is 'NONE'")
    }
}
@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Jul 17, 2020

Because one-off events shouldn't even be in a MutableStateFlow.

I hear that a LinkedListChannel exposed as a Flow would have the same effect, but without the magic.

Kotlin/kotlinx.coroutines@345458b

@AdamSHurwitz

This comment has been minimized.

Copy link

@AdamSHurwitz AdamSHurwitz commented Jul 17, 2020

Thank you for the recommendation! I implemented a LinkedListChannel to test it out and it works as expected to emit the one-time events.

The tradeoff is using the Channel introduces the need to launch the coroutine to utilize the Channel send and receive functions which creates a layer of nesting with either lifecycleScope.launch and viewModelScope.launch whereas with the Event, the value can be emitted with .value and returned with the extension function onEachEvent that tracks whether the event has been handled or not. This strategy also requires passing an additional argument into the Adapter, coroutineScope.

LinkedListChannel

FeedView.kt

@ExperimentalCoroutinesApi
interface FeedView {
    /**
     * Intent to select content from the feed
     *
     * @return A flow that emits the content to select from the feed
     */
     fun selectContent(): Channel<SelectContent?>
}

FeedFragment.kt

// LinkedListChannel passed into Adapter
val selectContent: Channel<SelectContent?> = Channel(UNLIMITED)

FeedAdapter.kt

private fun createOnClickListener(content: Content, position: Int) = OnClickListener { view ->
        when (view.id) {
            preview, contentTypeLogo ->
                coroutineScope.launch {
                    intent.selectContent.send(SelectContent(content, position))
                }
        }
    }

FeedViewModel.kt

fun bindIntents(view: FeedView) {
    viewModelScope.launch {
            view.selectContent().receive()?.let {
                selectContent(it)
            }
     }
}
@edujtm

This comment has been minimized.

Copy link

@edujtm edujtm commented Aug 29, 2020

Can't you just emit the event using offer() though? That way you don't need to create a CoroutineScope.

The SendChannel<T>.offer() documentation says this:

Immediately adds the specified [element] to this channel, if this doesn't violate its capacity restrictions,
and returns true. Otherwise, just returns false. This is a synchronous variant of [send] which backs off
in situations when send suspends.

and the LinkedListChannel, say this:

Channel with linked-list buffer of a unlimited capacity (limited only by available memory).
Sender to this channel never suspends and [offer] always returns true.

I've been using the event-emitter library from Zhuinden, but since I'm already using coroutines. I've been trying to implement the same behavior using only coroutines.

@mathroule

This comment has been minimized.

Copy link

@mathroule mathroule commented Oct 28, 2020

Thanks @JoseAlcerreca.

It's also possible to create an extension to MutableLiveData to directly post an Event:

fun <T> MutableLiveData<Event<T>>.postEvent(content: T) {
    postValue(Event(content))
}

And to replace usage with:

mutableLiveData.postValue(Event(data)) => mutableLiveData.postEvent(data)
@Artman12

This comment has been minimized.

Copy link

@Artman12 Artman12 commented Feb 2, 2021

Am I wrong or can't EventObserver handle null events? i.e. new Event<>(null). I'm not in Kotlin but when I got it right it?.getContentIfNotHandled()?.let(onEventUnhandledContent)

onEventUnhandledContent

is only called if event != null and the return value != null so with new Event<>(null)

onEventUnhandledContent

is never called?!

I've tweaked some code a bit to allow nulling the content. I'm new to kotlin so please correct me if there are any issues

open class Event<out T>(private val content: T) {
    var hasBeenHandled = false
        private set // Allow external read but not write

    fun handleContent() : T {
        hasBeenHandled = true
        return content
    }

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

class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.let {
            if (it.hasBeenHandled) return
            onEventUnhandledContent(it.handleContent())
        }
    }
}
@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Feb 3, 2021

@gmk57

This comment has been minimized.

Copy link

@gmk57 gmk57 commented Feb 21, 2021

I hear that a LinkedListChannel exposed as a Flow would have the same effect, but without the magic.

As you are probably aware it also needs a custom collector to work reliably.

I use https://github.com/Zhuinden/live-event now

This is fine solution too, supporting multiple observers and multiple queued events, but with a small catch: only one observer receives these queued events.

@Zhuinden

This comment has been minimized.

Copy link

@Zhuinden Zhuinden commented Feb 21, 2021

@gmk57 this is true. Theoretically it'd be possible to either expose setPaused from command-queue and/or provide a "minimumSubscriberCount" (working name) that would make it so that under 3 subscribers the EventEmitter is paused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment