Android Use LiveData To Show Toast Message From ViewModel

May 11, 2018

The following is a common solution to show Toast message from ViewModel.

Create a MutableLiveData to hold the toast message. To avoid MutableLiveData to be triggered multiple times, we use SingleLiveEvent instead.

class MainViewModel(): ViewModel() {
    internal val toastMessage = SingleLiveEvent<Int>()

    fun testToast() {
      toastMessage.value = R.string.hello
    }
}

SingleLiveEvent.kt (Source)

class SingleLiveEvent<T>() : MutableLiveData<T>() {

    private val pending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer<T> { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(t: T?) {
        pending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

    companion object {
        private const val TAG = "SingleLiveEvent"
    }
}

Create an observer for toastMessage in Activity class, probably at onCreate.

viewModel.toastMessage.observe(this, Observer { res ->
    if (res != null) {
       val message = getString(res)
       Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
})

NOTE: SingleLiveEvent only allow one observer.

The above solution is quite limited as we can only pass String resources. We can solve this limitation by implementing a ResourceString sealed classes which accept String resources, String and String resources with support arguments.

sealed class ResourceString {
    abstract fun format(context: Context): String
}
class IdResourceString(val id: Int): ResourceString() {
    override fun format(context: Context): String {
        return context.getString(id)
    }
}
class TextResourceString(val text: String): ResourceString() {
    override fun format(context: Context): String {
        return text
    }
}
class FormatResourceString(val id: Int, val values: Array<Any>): ResourceString() {
    override fun format(context: Context): String {
        return context.getString(id, *values)
    }
}

Activity

viewModel.toastMessage.observe(this, Observer { res ->
    if (res != null) {
       val message = res.format(context)
       Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
})

ViewModel

class MainViewModel(): ViewModel() {
    internal val toastMessage = SingleLiveEvent<ResourceString>()

    fun testToastWithResourceStringId() {
        toastMessage.value = IdResourceString(R.string.hello)
    }

    fun testToastWithString() {
        toastMessage.value = TextResourceString("Hello")
    }

    fun testToastWithResourceStringIdAndParameter() {
        // Hello, %1$s! You have %2$d new messages.
        toastMessage.value = FormatResourceString(R.string.hello_args, arrayOf("Desmond", 5))
    }
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.