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!

comments powered by Disqus