Pavel Poley
Pavel Poley

Reputation: 5607

Communication between view and ViewModel in MVVM with LiveData

What is a proper way to communicate between the ViewModel and the View, Google architecture components give use LiveData in which the view subscribes to the changes and update itself accordingly, but this communication not suitable for single events, for example show message, show progress, hide progress etc.

There are some hacks like SingleLiveEvent in Googles example but it work only for 1 observer. Some developers using EventBus but i think it can quickly get out of control when the project grows.

Is there a convenience and correct way to implement it, how do you implement it?

(Java examples welcome too)

Upvotes: 11

Views: 6786

Answers (5)

EpicPandaForce
EpicPandaForce

Reputation: 81588

You can easily achieve this by not using LiveData, and instead using Event-Emitter library that I wrote specifically to solve this problem without relying on LiveData (which is an anti-pattern outlined by Google, and I am not aware of any other relevant alternatives).

allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}

implementation 'com.github.Zhuinden:event-emitter:1.0.0'

If you also copy the LiveEvent class , then now you can do

private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter

fun doSomething() {
    emitter.emit("hello")
}

And

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewModel = getViewModel<MyViewModel>()
    viewModel.events.observe(viewLifecycleOwner) { event ->
        // ...
    }
}

// inline fun <reified T: ViewModel> Fragment.getViewModel(): T = ViewModelProviders.of(this).get(T::class.java)

For rationale, you can check out my article I wrote to explain why the alternatives aren't as valid approaches.

You can however nowadays also use a Channel(UNLIMITED) and expose it as a flow using asFlow(). That wasn't really applicable back in 2019.

Upvotes: 0

Daniel Nugent
Daniel Nugent

Reputation: 43342

For showing/hiding progress dialogs and showing error messages from a failed network call on loading of the screen, you can use a wrapper that encapsulates the LiveData that the View is observing.

Details about this method are in the addendum to app architecture: https://developer.android.com/jetpack/docs/guide#addendum

Define a Resource:

data class Resource<out T> constructor(
        val state: ResourceState,
        val data: T? = null,
        val message: String? = null
)

And a ResourceState:

sealed class ResourceState {
    object LOADING : ResourceState()
    object SUCCESS : ResourceState()
    object ERROR : ResourceState()
}

In the ViewModel, define your LiveData with the model wrapped in a Resource:

val exampleLiveData = MutableLiveData<Resource<ExampleModel>>()

Also in the ViewModel, define the method that makes the API call to load the data for the current screen:

fun loadDataForView() = compositeDisposable.add(
        exampleUseCase.exampleApiCall()
                .doOnSubscribe {
                    exampleLiveData.setLoading()
                }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        {
                            exampleLiveData.setSuccess(it)
                        },
                        {
                            exampleLiveData.setError(it.message)
                        }
                )
)

In the View, set up the Observer on creation:

    viewModel.exampleLiveData.observe(this, Observer {
        updateResponse(it)
    })

Here is the example updateResponse() method, showing/hiding progress, and showing an error if appropriate:

private fun updateResponse(resource: Resource<ExampleModel>?) {
    resource?.let {
        when (it.state) {
            ResourceState.LOADING -> {
                showProgress()
            }
            ResourceState.SUCCESS -> {
                hideProgress()
                // Use data to populate data on screen
                // it.data will have the data of type ExampleModel

            }
            ResourceState.ERROR -> {
                hideProgress()
                // Show error message
                // it.message will have the error message
            }
        }
    }
}

Upvotes: 0

tyczj
tyczj

Reputation: 74066

What about using Kotlin Flow?

I do not believe they have the same behavior that LiveData has where it would alway give you the latest value. Its just a subscription similar to the workaround SingleLiveEvent for LiveData.

Here is a video explaining the difference that I think you will find interesting and answer your questions

https://youtu.be/B8ppnjGPAGE?t=535

Upvotes: 2

patrick.elmquist
patrick.elmquist

Reputation: 2140

Yeah I agree, SingleLiveEvent is a hacky solution and EventBus (in my experience) always lead to trouble.

I found a class called ConsumableValue a while back when reading the Google CodeLabs for Kotlin Coroutines, and I found it to be a good, clean solution that has served me well (ConsumableValue.kt):

class ConsumableValue<T>(private val data: T) {
    private var consumed = false

    /**
     * Process this event, will only be called once
     */
    @UiThread
    fun handle(block: ConsumableValue<T>.(T) -> Unit) {
        val wasConsumed = consumed
        consumed = true
        if (!wasConsumed) {
            this.block(data)
        }
    }

    /**
     * Inside a handle lambda, you may call this if you discover that you cannot handle
     * the event right now. It will mark the event as available to be handled by another handler.
     */
    @UiThread
    fun ConsumableValue<T>.markUnhandled() {
        consumed = false
    }
}
class MyViewModel : ViewModel {
    private val _oneShotEvent = MutableLiveData<ConsumableValue<String>>()
    val oneShotEvent: LiveData<ConsumableValue<String>>() = _oneShotData

    fun fireEvent(msg: String) {
        _oneShotEvent.value = ConsumableValue(msg)
    }
}
// In Fragment or Activity
viewModel.oneShotEvent.observe(this, Observer { value ->
    value?.handle { Log("TAG", "Message:$it")}
})

In short, the handle {...} block will only be called once, so there's no need for clearing the value if you return to a screen.

Upvotes: 4

Tuan Dao
Tuan Dao

Reputation: 107

try 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
}

And wrapper it into LiveData

class ListViewModel : ViewModel {
    private val _navigateToDetails = MutableLiveData<Event<String>>()

    val navigateToDetails : LiveData<Event<String>>
        get() = _navigateToDetails


    fun userClicksOnButton(itemId: String) {
        _navigateToDetails.value = Event(itemId)  // Trigger the event by setting a new Event as a new value
    }
}

And observe

myViewModel.navigateToDetails.observe(this, Observer {
    it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
        startActivity(DetailsActivity...)
    }
})

link reference: Use an Event wrapper

Upvotes: 0

Related Questions