3

I have the following UI flow when searching items from a data source:

  1. Display a progress indicator while retrieving from source -> assign livedata to Outcome.loading(true)
  2. Display results -> assign LiveData Outcome.success(results)
  3. Hide progress indicator in -> assign LiveData Outcome.loading(false)

Now the problem is when #2 and #3 are called while the app is in the background. Resuming the app, the LiveData observers are only notified of #3 and not of #2 resulting to non-populated RecyclerView.

What is the correct way of handling this kind of situation?

class SearchViewModel @Inject constructor(
    private val dataSource: MusicInfoRepositoryInterface, 
    private val scheduler: Scheduler, 
    private val disposables: CompositeDisposable) : ViewModel() {

    private val searchOutcome = MutableLiveData<Outcome<List<MusicInfo>>>()
    val searchOutcomLiveData: LiveData<Outcome<List<MusicInfo>>>
        get() = searchOutcome

    fun search(searchText: String) {
        Timber.d(".loadMusicInfos")
        if(searchText.isBlank()) {
            return
        }

        dataSource.search(searchText)
                .observeOn(scheduler.mainThread())
                .startWith(Outcome.loading(true))
                .onErrorReturn { throwable -> Outcome.failure(throwable) }
                .doOnTerminate { searchOutcome.value = Outcome.loading(false) }
                .subscribeWith(object : DisposableSubscriber<Outcome<List<MusicInfo>>>() {
                    override fun onNext(outcome: Outcome<List<MusicInfo>>?) {
                        searchOutcome.value = outcome
                    }

                    override fun onError(e: Throwable) {
                        Timber.d(e, ".onError")
                    }

                    override fun onComplete() {
                        Timber.d(".onComplete")
                    }
                }).addTo(disposables)
    }

    override fun onCleared() {
        Timber.d(".onCleared")
        super.onCleared()
        disposables.clear()
    }
}

And below is my Outcome class

sealed class Outcome<T> {
    data class Progress<T>(var loading: Boolean) : Outcome<T>()
    data class Success<T>(var data: T) : Outcome<T>()
    data class Failure<T>(val e: Throwable) : Outcome<T>()

    companion object {
        fun <T> loading(isLoading: Boolean): Outcome<T> = Progress(isLoading)

        fun <T> success(data: T): Outcome<T> = Success(data)

        fun <T> failure(e: Throwable): Outcome<T> = Failure(e)
    }
}
Mark Pazon
  • 6,167
  • 2
  • 34
  • 50

2 Answers2

7

You should not make your loading state a "double" state (true/false). Your progress state should be dispatch only when loading, then you go either on success or failure state. Never go back to loading state at the end. Doing so you always know which state your view need to display.

  • if loading -> show loader
  • if success -> hide loader, show data
  • if error -> hide loader, show error

Here is an example extract from my Android Conductor + MVVM + Dagger project template, it uses conductor but you can replace conductor controller with fragment or activity, that's the same logic.

sealed class DataRequestState<T> {
    class Start<T> : DataRequestState<T>()
    class Success<T>(var data: T) : DataRequestState<T>()
    class Error<T>(val error: Throwable) : DataRequestState<T>()
}

ViewModel:

@ControllerScope
class HomeControllerViewModel
@Inject
constructor(homeRepositoryManager: HomeRepositoryManager) : BaseControllerViewModel(),
    DataFetchViewModel<Home> {
    private val _dataFetchObservable: DataRequestLiveData<Home> =
        DataRequestLiveData(homeRepositoryManager.home())
    override val dataFetchObservable: LiveData<DataRequestState<Home>> = _dataFetchObservable

    override fun refreshData() {
        _dataFetchObservable.refresh()
    }
}

Base data Controller (fragment/activity/conductor):

abstract class BaseDataFetchController<VM, D> :
    BaseViewModelController<VM>() where VM : BaseControllerViewModel, VM : DataFetchViewModel<D> {
    override fun onViewCreated(view: View) {
        super.onViewCreated(view)

        viewModel.dataFetchObservable.observe(this, Observer {
            it?.let {
                when (it) {
                    is DataRequestState.Start -> dataFetchStart()
                    is DataRequestState.Success -> {
                        dataFetchSuccess(it.data)
                        dataFetchTerminate()
                    }
                    is DataRequestState.Error -> {
                        dataFetchError(it.error)
                        dataFetchTerminate()
                    }
                }
            }
        })
    }

    protected abstract fun dataFetchStart()
    protected abstract fun dataFetchSuccess(data: D)
    protected abstract fun dataFetchError(throwable: Throwable)
}
Tomasz Dzieniak
  • 2,765
  • 3
  • 24
  • 42
Samuel Eminet
  • 4,647
  • 2
  • 18
  • 32
2

Loading state and loaded data should be strictly separate, and you should maintain two live datas and two observers.

That way, loading == false and you'll receive latest data on re-subscription.

Think about it: loading state isn't really an outcome.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Thanks. A better class name would be State. I've seen this pattern in a couple of github repositories but some have not handled the progress indicator well. Not sure if having an extra livedata object just for the purpose of a progress indicator is efficient but that could be a workaround. – Mark Pazon May 10 '18 at 15:41
  • Optimize for correctness, not for minimizing the number of MutableLiveData imo – EpicPandaForce Jan 25 '21 at 11:58