Exploring `NetworkingViewState` from KotlinConf 2017

KotlinConf 2017 was abundant with great talks. One of my favorites was View State Machine for Network Calls on Android by Amanda Hill. If you haven’t had the opportunity, definitely check it out.

The Prelude

The development of the Game of Cones presentation explores the how expanding requirements affect a code base. My takeaway from the presentation is this: We need a way to reduce the ripple effect of a requirement change.

In this case, the “product owner” has asked for an existing screen be updated. This caused not only the UI (layout) to be updated, but also the data model, UI interface, presenter, and tests. Amanda’s solution is to create a sealed class she calls NetworkingViewState. Here is that class:

sealed class NetworkingViewState {
    class Init : NetworkingViewState()
    class Loading : NetworkingViewState()
    class Success<out T>(val item :T): NetworkingViewState()
    class Error(val errorMessage: String?): NetworkingViewState()
}

I loved the idea of encapsulating the state of the screen and I wanted to play with the idea. Thanks for the inspiration, Amanda!

The Implementation

Amanda didn’t have a repo published with the code, so I decided to create my own.

Having only the slides to go on, I had to fill in a few blanks. Specifically, the evaluation of the state object when it changed. Here is what I came up with:

override var networkingViewState: NetworkingViewState
        by Delegates.observable<NetworkingViewState>(
                NetworkingViewState.Init()) { _, _, newValue ->
            when (newValue) {
                is NetworkingViewState.Loading -> {
                    titleView.text = getString(R.string.loading)
                    caloriesView.text = null
                    iconView.setImageURI(null)
                }
                is NetworkingViewState.Success<*> -> {
                    val item = newValue.item as? IceCreamViewModel
                    titleView.text = item?.title()
                    iconView.setImageURI(Uri.parse(item?.iconUrl()))
                    caloriesView.text = item?.calorieCount()
                }
                is NetworkingViewState.Error -> {
                    titleView.text = getString(R.string.error)
                    caloriesView.text = newValue.errorMessage
                    iconView.setImageURI(null)
                }
            }
        }

Please direct your attention to the success block:

is NetworkingViewState.Success<*> -> {
    val item = newValue.item as? IceCreamViewModel
    ...
}

In order for this to compile, I had to handle the erasure of the Success type parameter by making it a wildcard. I then did a safe cast of newValue.item to IceCreamViewModel. The type erasure is potentially problematic, but the likelihood of the erased type causing a crash is mitigated by the as? syntax. If the item is not an IceCreamViewModel, it will be null.

I now have a fully-coded application comprised of the code from the slides and the code I inferred. The only thing we are missing is the API service, which I don’t plan to implement at this time.

The Tests

With all the code in place, we can check the tests…

➜  ./gradlew test

> Task :app:testDebugUnitTest

com.codeprogression.gameofcones.MainPresenterTest > test_OnCreate_success FAILED
    java.lang.ExceptionInInitializerError at MainPresenterTest.kt:33
        Caused by: java.lang.RuntimeException at MainPresenterTest.kt:33

com.codeprogression.gameofcones.MainPresenterTest > test_OnCreate_error FAILED
    java.lang.NoClassDefFoundError at MainPresenterTest.kt:46

3 tests completed, 2 failed

As you can see, two of the three tests pass. The IceCreamViewModelTest.testTitle() passes. However, the two presenter tests fail. The success test failed with an initialization exception and the error test failed with a class-not-found exception. In actuality, these are both failing because the observeOn() operator is expecting an Android-based type: AndroidSchedulers.mainThread().

If we look at the test report, we can find a clue as to the problem. The method getMainLooper() was not mocked.

java.lang.ExceptionInInitializerError
...
Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper
 not mocked. See http://g.co/androidstudio/not-mocked for details.
	at android.os.Looper.getMainLooper(Looper.java)
	at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder
    .<clinit>(AndroidSchedulers.java:29)

The linked article tells us the following:

The android.jar file that is used to run unit tests does not contain any actual code - that is provided by the Android system image on real devices. Instead, all methods throw exceptions (by default). This is to make sure your unit tests only test your code and do not depend on any particular behaviour of the Android platform (that you have not explicitly mocked e.g. using Mockito).

To “fix” the problem, we can add the following:

android {
  // ...
  testOptions {
    unitTests.returnDefaultValues = true
  }
}

And with this change, all of our tests pass:

➜  ./gradlew test

BUILD SUCCESSFUL in 1s

The Warning

Upon closer inspection of the advice from the article, we are given a warning: [Throwing exceptions by default] is to make sure your unit tests only test your code and do not depend on any particular behavior of the Android platform.

We have a choice to make. Do we heed the warning and change our implementation? Or is this implementation good enough?

See Part 2

comments powered by Disqus