Exploring `NetworkingViewState`, Part 2
In Part 1, I created a repository to explore the Amanda Hill’s NetworkingViewState
pattern. When running the tests, I came across an exception that we solved in a controversial way. In this post, I would like to explore an alternative option.
During the presentation, Amanda declared that she had the mic which means it was her opinion that mattered. I agree…it is her project and her prerogative to architect her application in the manner presented. I am only offering an alternative approach that you, dear reader, can accept or reject (in whole or in part).
The Pattern: MVP
I am a strong proponent of the Model-View-Presenter pattern in Android. Some of my coworkers might even argue that I’m am overzealous with my application of the pattern. Here is why… I am very strict about which classes may use the Android framework (the android.*
imports). In my opinion (right or wrong), the Android framework should primarily be limited to classes that extend the Android framework (such as Activity, View, or Application). Many of these framework classes are not final classes and cannot be mocked without PowerMock or tested without Robolectric. Therefore, as a general practice, I do not allow framework classes to be imported into presenters or model classes.
With this in mind, there are a couple changes I want to make.
- Remove the presenter’s dependency on
AndroidSchedulers
- Encapsulate the update of the
NetworkingViewState
property
The Refactor
To remove this dependency, we can change the presenter’s responsibility. The presenter is currently responsible for updating the state on the view in its onCreate
method. Because it is updating the view, the Rx callbacks need to be scheduled on the main thread. This is what caused the exception from Part 1. Let’s change this to return the view state as an Observable
and change the methodName to loadCone()
.
fun loadCone(): Observable<NetworkingViewState>
The presenter uses the data store to get an IceCream
. It then transforms it to a subclass of NetworkingViewState
.
fun loadCone(): Observable<NetworkingViewState> {
return Observable.concat(
// Start with loading state
Observable.just(NetworkingViewState.Loading()),
// get the cone
dataStore.fetchCone()
.subscribeOn(Schedulers.io())
// On success, return the success state with a view model
.map { iceCream ->
NetworkingViewState.Success(
IceCreamViewModel(context, iceCream)) as NetworkingViewState
}
// On failure, return the error state with a message
.onErrorReturn {
NetworkingViewState.Error(it.message)
}.toObservable())
}
The presenter still has a dependency on android.content.Context
, which I just mentioned I usually don’t allow. Because Context
is an abstract class, it is mockable in the tests. I’m amenable to leaving the dependency for now. I’ll return to this in Part 3. In the tests, we verify our view states are published correctly through the observable. We aren’t checking whether the view’s state property was updated. In other words, we are testing the presenter, not the view.
@Test
fun test_OnCreate_success() {
whenever(dataStore.fetchCone()).thenReturn(Single.just(fakeIceCream))
val presenter = MainPresenter(context, dataStore)
presenter.loadCone().test()
.assertNoErrors()
.assertComplete()
.assertValueAt(0) { it is NetworkingViewState.Loading }
.assertValueAt(1) {
it is NetworkingViewState.Success<*>
&& it.item == IceCreamViewModel(context, fakeIceCream)
}
}
Now that the presenter doesn’t update the view, we can remove the MainView
interface. We also need to have the activity take on the responsibility of setting the networkingViewState
property. In the onCreate()
function, the activity will subscribe to presenter.loadCone()
. When the presenter calls onNext
, the activity’s subscription updates the property.
presenter.loadCone()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { state -> networkingViewState = state }
Review
Here is what we achieved with this small refactor:
- We no longer need our main-looper workaround for the test, because our presenter no longer depends on
AndroidSchedulers
. - Our presenter tests now only test the presenter logic, not the view logic
It may not seem like a lot, but it sets us up for our next steps.
Next Steps
Unfortunately, we now have code in the activity (view) that isn’t tested anymore. We are no longer testing the networkingViewState
property’s setter logic. We should be able to fix that problem. Stay tuned for Part 3!