Single event MutableLiveData regarding API result refactor

from the CommonsWare Community archives

At June 22, 2021, 6:59am, grrigore asked:

Hello,

I’m dealing with MutableLiveData and single events. As an example, once a request is completed, but it throws an error “missing username” (in case of a login fragment), I will use a popup to display this. If I navigate to another fragment and then return to the login fragment, the popup will be shown again. I’ve found a way to display it only once but I don’t really like the solution.

Event.kt

/**
 * 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) {

    private var hasBeenHandled = false

    /**
     * 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
}

Result.kt

data class  Result<out T>(val status: Status,
                         val data: T?,
                         val message: String? = null,
                         val exception: Exception? = null) {

    enum class Status {
        SUCCESS,
        ERROR,
        LOADING
    }

    companion object {
        fun <T> success(data: T): Result<T> {
            return Result(
                Status.SUCCESS,
                data
            )
        }

        fun <T> error(message: String, exception: Exception? = null, data: T? = null): Result<T> {
            return Result(
                Status.ERROR,
                data,
                message,
                exception
            )
        }

        fun <T> loading(data: T? = null): Result<T> {
            return Result(
                Status.LOADING,
                data
            )
        }
    }
}

Event.kt is used to get the content only once, while Result.kt is used as a wrapper to handle the request’s states.

In my view model I’m using:

private val _startRideResult = MutableLiveData<Event<Result<RideDetailsResponseModel?>>>()
val startRideResult: LiveData<Event<Result<RideDetailsResponseModel?>>>
    get() = _startRideResult

While in my fragment I’m using:

viewModel.startRideResult.observe(viewLifecycleOwner, { event ->
        event.getContentIfNotHandled()?.let { result ->
        when (result.status) {
            Result.Status.LOADING -> showProgress()
            Result.Status.SUCCESS -> {
                hideProgress()
                findSafeNavController().navigate(HitchRideFragmentDirections.actionNavigationHitchRideToNavigationScanForRide())
            }
            Result.Status.ERROR -> {
                hideProgress()
                result.message?.let {
                    when (result.exception) {
                        is NetworkException -> findSafeNavController().navigate(R.id.navigation_internet)
                        else -> viewModel.setError(it)
                    }
                } ?: run {
                    viewModel.setTranslationError(
                        R.string.default_error_message,
                        getString(R.string.key_default_error_message)
                    )
                }
            }
        }
    }
})

The thing I don’t really like it’s the use of LiveData<Event<Result<RideDetailsResponseModel?>>> there are a lot of nested diamond operators and I think there is something I could refactor in order to get a better version of this, but I don’t really know where to start.

Any tips are appreciated. Thanks!


At June 23, 2021, 12:27am, mmurphy replied:

That is basically the “single live event” pattern. AFAIK, it is still the best option for LiveData.

I’m more of a sealed class fan for something like Result, but that will not address your issue. I suppose you could come up with a way to have Event-like characteristics in your Result, to eliminate the separate Event in your declaration. However, that only makes sense if Result only is used as an event, and that probably is not the case. So, in terms of the literal code, what you have is probably fine, as each level represents a different concern.

If your issue is just with all the angle brackets, you could use typealias:

typealias RideDetailsResponseResult = Result<RideDetailsResponseModel?>

or:

typealias RideDetailsResponseEvent = Event<Result<RideDetailsResponseModel?>>