I’m trying to observe the loading state of a page. However, I make 2 API calls on my ViewModel. I want to display a progress bar until both of the items are loaded.
I have a sealed class to indicate the loading state of the data, which goes like:
sealed class DataState<out R> {
data class Success<out T>(val data: T) : DataState<T>()
data class Error(val exception: Exception) : DataState<Nothing>()
object Loading : DataState<Nothing>()
}
My view model:
init {
getData1()
getData2()
}
val data1 = MutableLiveData<List<model1>>()
val data2 = MutableLiveData<List<model2>>()
private fun getData1() {
viewModelScope.launch {
data1.postValue(DataState.Loading)
val result = try {
repository.getData1().data!!
}
catch (e: Exception) {
data1.postValue(DataState.Error(e))
return@launch
}
data1.postValue(DataState.Success(result))
}
}
private fun getData2() {
viewModelScope.launch {
data1.postValue(DataState.Loading)
val result = try {
repository.getData2().data!!
}
catch (e: Exception) {
data2.postValue(DataState.Error(e))
return@launch
}
data2.postValue(DataState.Success(result))
}
}
I wanna observe a live data so that I can see that both of the states are successful. Is that possible?
CodePudding user response:
You probably want a MediatorLiveData
:
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
That's the example from the docs - it's a very simple one that just sets a single value on liveDataMerger
when either of the source LiveDatas posts a new value.
There's an example on the Android Developers blog that's closer to what you want:
...
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
...
private fun combineLatestData(
onlineTimeResult: LiveData<Long>,
checkinsResult: LiveData<CheckinsResult>
): UserDataResult {
val onlineTime = onlineTimeResult.value
val checkins = checkinsResult.value
// Don't send a success until we have both results
if (onlineTime == null || checkins == null) {
return UserDataLoading()
}
// TODO: Check for errors and return UserDataError if any.
return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
so every time you get a new value on one of the LiveDatas, you pass them both to a function that does some validation and returns a current state.
I should also point out that Flow
s are recommended for a lot of things now, and the combine
function (which does the same kind of thing as MediatorLiveData
) is a bit easier to read (#5 in that article). Just so you know! Either is good here
CodePudding user response:
You don't need to have separate LiveData
of DataState
for each API Call.
Rather than this:
val data1 = MutableLiveData<List<model1>>()
val data2 = MutableLiveData<List<model2>>()
You can have just this:
private val _stateLiveData = MutableLiveData<DataStates<model3>>()
val statesLiveData: LiveData<DataStates<model3>>
get() = _stateLiveData
Consider model3
as a data class
that aggregates both model1
and model2
. It can be used to aggregate other data fields that can be used in future.
data class model3(
var data1: model1?,
var data2: model2?
)
This way, when you observe the states, maintaining states becomes simpler:
viewModel.statesLiveData.observe(this) { states ->
when(states) {
is DataStates.Error -> // Handle Error here.
DataStates.Loading -> // Handle Loading here.
is DataStates.Success -> // Handle Success here.
}
}
One more thing, since the methods getData1()
and getData2()
are launching Coroutines separately, the REST API Calls are not in sequence of one another. And it's not guaranteed which call would finish first, that can lead to extra code just to maintain the progress until both are completed.
This can be solved easily by sequencing the REST API Calls itself, by merging the data fetch operation into one method under one CoroutineScope
.
To summarize:
class YourViewModel : ViewModel() {
private val _stateLiveData = MutableLiveData<DataStates<model3>>()
val statesLiveData: LiveData<DataStates<model3>>
get() = _stateLiveData
init {
getData()
}
private fun getData() {
viewModelScope.launch(Dispatchers.IO) {
notifyState(DataStates.Loading)
try {
val result1 = model1(1)
val result2 = model2(2)
notifyState(
DataStates.Success(
model3(
result1,
result2
)
)
)
} catch (e: Exception) {
notifyState(DataStates.Error(e))
}
}
}
private fun notifyState(state: DataStates<model3>) {
viewModelScope.launch(Dispatchers.Main) {
_stateLiveData.value = state
}
}
}