Search code examples
androidretrofitmutablelivedata

Is there any solution to observe a MutubleLiveData's state in View while it is changed through method's perameter in ViewModel


MutableLiveData's state is not observed in Fragment while I pass a reference of its(MutableLiveData) instance through a method in ViewModel. Only http call is happened and value is shown in log, NO action corresponding LiveData's State is not observed

I want to call a Http method which logic is written in LoginViewModel which is a child class of BaseViewModel. In BaseViewModel I created some common methods which take MutableLiveData as parameter, call those method in LoginViewModel's method and observe those LiveData in Fragment

UiState.kt

sealed class UiState<T> {
    data class Progress<T>(val isLoading: Boolean) : UiState<T>()
    data class Success<T>(val successInfo: T) : UiState<T>()
    data class Failure<T>(val throwable: Throwable) : UiState<T>()
    data class Alert<T>(val alert: String) : UiState<T>()

    companion object {
        fun <T> loading(isLoading: Boolean): UiState<T> = Progress(isLoading)
        fun <T> success(successInfo: T): UiState<T>? = Success(successInfo)
        fun <T> failure(throwable: Throwable): UiState<T> = Failure(throwable)
        fun <T> alert(alert: String): UiState<T> = Alert(alert)
    }
}

Event.kt

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

    private var hasBeenHandled = false

    fun getContentIfNotHandled() = if (hasBeenHandled) {
        null
    } else {
        hasBeenHandled = true
        content
    }

    fun peekContent() = content
}

BaseViewModel.kt

fun <T> onSuccessHttpResponse(state: MutableLiveData<Event<UiState<T>>>) = Consumer<Response<T>> {
    state.value = Event(loading(true))

    if (it.isSuccessful) {
        state.value = Event(loading(false))
        state.value = Event(success(it.body()!!)!!)
    } else {
        val error = Gson().fromJson(it.errorBody()?.charStream(), ApiError::class.java)

        when (it.code()) {
            Constants.ACCESS_TOKEN_REFRESH_STATUS_CODE -> state.value = Event(alert("Renew Access Token please"))
            Constants.CUSTOM_STATUS_CODE -> state.value = Event(alert(error.message!!))
            else -> state.value = Event(alert("Something went wrong"))
        }

        state.value = Event(loading(false))
    }
}

fun <T> onErrorHttpResponse(state: MutableLiveData<Event<UiState<T>>>) = Consumer<Throwable> {
    state.value = Event(loading(false))
    state.value = Event(UiState.failure(it))
}

fun <T> inputNotFoundError(state: MutableLiveData<Event<UiState<T>>>) {
        state.value = Event(loading(false))
        state.value = Event(alert("Please Filled all Info"))
    }

LoginViewModel.kt

val tutorLoginState: MutableLiveData<Event<UiState<TutorLoginResponse>>> = MutableLiveData()

fun tutorLogin(loginInfo: LoginInfo) {
    if (loginInfo.isAssigned()) {
        callLoginTutorApi(loginInfo)
    } else {
        inputNotFoundError(tutorLoginState)
    }
}

private fun callLoginTutorApi(loginInfo: LoginInfo) {
    compositeDisposable += userLoginService.tutorLogin(loginInfo)
        .performOnBackgroundOutputOnMain()
        .subscribe({
            onSuccessHttpResponse(tutorLoginState)
        }, {
            onErrorHttpResponse(tutorLoginState)
        })
}

LoginFragment.kt

override fun observeLiveData() {
    viewModel.tutorLoginState.observe(this, Observer {
        it.getContentIfNotHandled()?.let { state ->
            when (state) {
                is UiState.Progress -> {
                    if (state.isLoading) {
                        network_loading_indicator.visible()
                    } else {
                        network_loading_indicator.visibilityGone()
                    }
                }

                is UiState.Success -> {
                    val responseData: TutorInfo = state.successInfo.data?.tutorInfo!!
                    context?.showToast(responseData.tutorName.toString())
                }

                is UiState.Alert -> context?.showToast(state.alert)

                is UiState.Failure -> {
                    if (state.throwable is IOException) {
                        context?.showToast("Internet Connection Failed")
                    } else {
                        context?.showToast("Json Parsing Error")
                    }
                }
            }
        }
    })

Only Http call is happened. But no response in the change of LiveData


Solution

  • Based on the prior conversation, you can handle the disposables in your BaseViewModel like this and create a LiveData value loader to handle it centrally inside the base and also, to reuse the same single live event in every API call & a message to show error in Toast or handle like this:

    BaseViewModel

    abstract class BaseViewModel : ViewModel() {
        protected val compositeDisposable = CompositeDisposable()
        val loader: MutableLiveData<Boolean> by lazy { SingleLiveEvent<Boolean>() }
        val message: MutableLiveData<Message> by lazy { SingleLiveEvent<Message>() }
        override fun onCleared() {
            compositeDisposable.clear()
            super.onCleared()
        }
    }
    

    In the ViewModel, keep a LiveData for the response and a disposable of it. Just toggle the loader value here so that it can be observed from the Fragment/Activity to toggle the loader visibility by using doOnSubscribe() and doOnTerminate() like the following (a detail explanation into using this RxJava action operators can found here). Basically to sum it up:

    • doOnSubscribe() — Modifies the source so that it invokes the given action when it is subscribed from its subscribers.
    • doOnTerminate() — Calls the specified action just before this Observable signals onError or onCompleted.

    LoginViewModel

    private lateinit var disposableResponse: Disposable
    val tutorLoginResponse = MutableLiveData<TutorLoginResponse>()
    
    fun login() {
           disposableResponse = userLoginService.tutorLogin()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { loader.value = true }
                .doOnTerminate { loader.value = false }
                .subscribe({
                        onRetrieveResponseSuccess(it)
                    }, {
                        onRetrieveResponseError(it)
                    })
    
            compositeDisposable.add(disposableResponse)
        }
    
    private fun onRetrievePostListSuccess(response: TutorLoginResponse) {
            tutorLoginResponse.value = response
    }
    
    private fun onRetrievePostListError(error: Throwable) {
            message.value = ToastMessage(error.message) //ToastMessage is a simple utility class to show Toast
    }
    

    Then observe the updated LiveData value of loader from your viewmodel and toggle the visibility of your loader in UI from your Activity/Fragment & also access the response like this:

    LoginFragment

    viewModel.loader.observe(this, Observer {
                if(it) showLoader() //showLoader() is a simple method in BaseFragment which invokes .show() on your deafult or custom Lottie animated loader
                else hideLoader() //hideLoader() is a similar method for invoking .hide() on your loader
    })
    
    viewModel.tutorLoginResponse.observe(this, Observer { response ->
                //do whatever with your response that's been returned here
    })