A State of MVVM
A State of MVVM
Since Google introduced architectural components, developers are trying now to adopt MVVM pattern. ViewModels with LiveData can help engineers in designing solutions a lot, so that the amount of boilerplate code is significantly lower.
Multiple observers
I was using LiveData and have discovered an issue which bothers me. Imagine the case when ViewModel provides several methods for returning LiveData. In this case the view (Activity or Fragment) has to observe multiple LiveData sources and react appropriately. When everything is handled on the UI thread it is not a big issue, however, it can create issues with understanding the data flows. Using LiveData for observing loading progress, loaded data and occurred error, requires having 3 separate instances of LiveData. Let us look at the following example:
class WorkoutsViewModel : ViewModel() {
val loading : LiveData<Boolean>
val error: LiveData<Throwable>
val workouts : LiveData<List<Workout>>
}
It is easy to make a mistake when there is a need to observer multiple LiveData objects. We have to manage 3 sources of data providers and perform synchronization between all of them. Also we need to make sure they are in correct order and we don’t have “race conditions” in states.
Single State
A solution which could solve the issue is to have a single source of truth. Wouldn’t it be good to have only one LiveData and a view would observe only it? In this case, ViewModel in this case submits all possible types of data (states) to the single LiveData. Let’s look at the possible variations of states below.
sealed class WorkoutState {
object Loading : WorkoutState()
data class Error(val message: String?) : WorkoutState()
data class WorkoutsLoaded(val workouts: List<Workout>) : WorkoutState()
}
Now the ViewModel has only one instance of LiveData of type parent (sealed) class. Any data or actions needed for the view can now be passed through the internal instance of MutableLiveData
using the method setValue. I would recommend using setValue
method rather then postValue. You will need to be sure that the method setValue
is called on the main thread, but the state will not be lost as could happen with the sequence of calls of the method postValue
. This would be because it can post values from different threads to the observer.
class WorkoutsViewModel(private val restClient: restClient) : ViewModel() {
private val internalState = MutableLiveData<WorkoutState>()
val state : LiveData<WorkoutState> = internalState
fun reloadWorkouts() {
internalState.value = Loading
restClient.getWorkouts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ workouts -> internalState.value = WoroutsLoaded(workouts) },
{ throwable -> internalState.value = Error(throwable.message) })
}
}
In the example above we are loading a list of workouts from our REST API using RxJava. Our first step of the state is Loading
, once the data has been received, we want to set the state WorkoutsLoaded
. If error occurs, the state should set at Error
and an error message should be received.
This view should only observe a single state. If the view is Activity, then observing should happen in onCreated()
callback. For Fragments the callback onViewCreated()
can be used.
viewModel.state.observe(this, Observer { state ->
when (state) {
is Loading -> showProgress()
is Error -> showError(state.message)
is WorkoutsLoaded -> updateWorkouts(state.workouts)
}
})
Testing
Only having a single state makes ViewModel testing relatively easy. The main scenario will be the following: an appropriate function should be called and then received states should be verified. Since this is the single primary “pipeline” of communication to the View, we can be sure what exactly is passed to the View. Let’s check one of the tests of the ViewModel.
class TestWorkoutsViewModel {
@get:Rule
var testRule: TestRule = InstantTaskExecutorRule()
private lateinit var workoutsViewModel: WorkoutsViewModel
private val stateObserver = mock<Observer<WorkoutState>>()
private val mockRestClient = mock<RestClient>()
@Before
fun setup() {
workoutsViewModel = WorkoutsViewModel(mockRestClient)
workoutsViewModel.state.observeForever(stateObserver)
}
@Test
fun `when the list of workouts reloaded successfully`() {
// given
val responseData = readResponseFromFile()
whenever(restClient.getWorkouts()).thenReturn(Single.just(responseData))
// when
workoutsViewModel.reloadWorkouts()
// then
inOrder(stateObserver) {
verify(stateObserver).onChanged(WorkoutState.Loading)
verify(stateObserver).onChanged(WorkoutState.WorkoutsLoaded(responseData))
}
verifyNoMoreInteractions(stateObserver)
}
}
Before running a test, make sure to do setup and preparation, such as mocking and creating appropriate structures. Don’t forget to add a test rule InstantTaskExecutorRule, which is needed for testing Architectural Components. By using the method observerForever()
we will be able to start observing the submitted data. We can then verify that the states are submitted in the appropriate order and that nothing else except expected states is received.