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 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 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.

@AlexBacich

This comment has been minimized.

Copy link

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 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 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 commented Feb 26, 2019

@WebTiger89 yes you are correct, not completely ideal

@ildar2

This comment has been minimized.

Copy link

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 commented Apr 28, 2019

Thank you

@OssamaDroid

This comment has been minimized.

Copy link

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 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 commented Dec 4, 2019

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

@erfanegtfi

This comment has been minimized.

Copy link

erfanegtfi commented Jan 14, 2020

Hi, can someone convert above code to java?

@Zhuinden

This comment has been minimized.

Copy link

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 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 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 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 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 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 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". 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.