Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

A View State Machine for Network Calls on Android

DZone's Guide to

A View State Machine for Network Calls on Android

In this post we take a look at how you can handle network requests in Android in order to protect yourself from UI changes. Read on for details!

· Mobile Zone
Free Resource

Get gorgeous, multi-touch charts for your iOS application with just a few lines of code.

As Ben Franklin once said, “In this world, nothing can be said to be certain, except death and taxes [and that software design specs always change].”

The reality of ever-changing design specs can, at times, be frustrating and miserable, but rather than focus on the negatives I like to think that it can also be a fun challenge in how we approach development. So today I am going to share one improvement I made on how I handle network requests in Android development to protect against UI changes.

MVP

MVP (Model View Presenter) is a popular architectural style for Android development. And while there are many positives - easy to test, good separation between views, and model - there are also some downsides. One particular frustration I have faced is having to update the view interface every time you want to update another UI element. For example, if you were building a profile page you might have the following view interface:

interface ProfileView {
  fun showFirstName(name: String)
  fun showLastName(name: String)
}


But if you wanted to update the UI to also show a user’s age and astrological sign you would also have to update the view interface.

interface ProfileView {
  fun showFirstName(name: String)
  fun showLastName(name: String)
  fun showAge(age: Int)
  fun showAstrologicalSign(sign: String)
}


This update to the view interface is both frustrating and a violation of the Open-Closed Principle.

Note: Before I continue talking about how annoying updating view interfaces can be, I want to admit that, just like everything else in life, there are pros and cons, and sometimes this sort of update is necessary and can even be good.

But enough mature pragmatism, back to the bad. Another reason I find these sorts of updates frustrating is that they can take away from the bigger picture of what is happening in a given view and focus too much on the minute details of visual implementation. Now I know what you’re thinking, “but it’s a VIEW INTERFACE! It’s supposed to focus on the visuals!” And you would be correct, but before you get a bee in your bonnet consider that we do not yet have the full picture. So far in our example, we have only been focusing on the methods that handle displaying a User. Let’s look at the presenter, to see how it fetches the User to better understand the rest of what is happening with this view.

class ProfilePresenter(val view: ProfileView, val api: ProfileDataStore) {

  fun fetchUser() {
    api.fetchUser()
        .subscribe({ user ->
          // network call successful           view.showFirstName(user.firstName)
          view.showLastName(user.lastName)
          view.showAge(user.age)
          view.showAstrologicalSign(user.sign)
        }, { error ->
          // network call failed         })
  }
}


Ah, good ole network calls. Because our User object is coming from a network call, we need to update our view interface to include methods for showing the various states surrounding making a request.

interface ProfileView {
  fun showFirstName(name: String)
  fun showLastName(name: String)
  fun showAge(age: Int)
  fun showAstrologicalSign(sign: String)
+ fun showError(errorMessage:String) + fun showLoading() + fun hideLoading() }


Our view interface is starting to look a bit different. It’s not all about the User details anymore, it also has some networking view state stuff in there. As you can imagine, all view interfaces that work with a presenter that makes networking calls would also have to add these networking related view methods. That means a lot of duplication across all our view interfaces and no one wants that.

Networking View State

Say hello to NetworkingViewState. A sealed class, Networking View State abstracts all the various states a view can be in, as a result of a networking call, into a single type.

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


Now let’s update our view interface with this new type.

interface ProfileView  {
  var networkingViewState: NetworkingViewState
}


And our presenter:

class ProfilePresenter(val view: ProfileView, val api: ProfileDataStore) {

  fun fetchUser() {
    view.networkingViewState = NetworkingViewState.Loading()
    api.fetchUser()
        .subscribe({ user ->
          // network call successful           view.networkingViewState = NetworkingViewState.Success<User>(user)
        }, { error ->
          // network call failed           view.networkingViewState = NetworkingViewState.Error(error.message)
        })
  }
}


Now if the UI changes, our view interface, our presenter, and our tests do not have to change because the bigger picture behavior remains the same.

But we're not done yet. While we have made our code more SOLID (get it!?) we have also lost some test coverage. Before we introduced NetworkingViewState our presenter was in charge of mapping the User object we got in the response from the API into the appropriate types to be shown by the view interface. And because those formatted values were passed through the view interface we could assert they were the correct values in our tests.

Like this:

class ProfilePresenterTest() {

  val view = mock<ProfileView>()
  val dataStore = mock<ProfileDataStore>()
  val mockUser = User("Amanda", "Hill", 100, "Aquarius")

  @Test
  fun testFetchUser_success() {
    //stub network response     whenever(dataStore.fetchUser()).thenReturn(Observable.just(mockUser))

    val presenter = ProfilePresenter(view, dataStore)
    presenter.fetchUser()

    verify(view).showLoading()
    verify(view).showFirstName(mockUser.firstName)
    verify(view).showLastName(mockUser.lastName)
    verify(view).showAge(mockUser.age)
    verify(view).showAstrologicalSign(mockUser.sign)
    verify(view).hideLoading()
  }
}


But with our updated presenter our tests now look like this:

class ProfilePresenterTest() {

  val view = mock<ProfileView>()
  val dataStore = mock<ProfileDataStore>()
  val mockUser = User("Amanda", "Hill", 100, "Aquarius")

  @Test
  fun testFetchUser_success() {
    //stub network response     whenever(dataStore.fetchUser()).thenReturn(Observable.just(mockUser))

    val presenter = ProfilePresenter(view, dataStore)
    presenter.fetchUser()

    verify(view).networkingViewState = isA<NetworkingViewState.Loading>()
    verify(view).networkingViewState = isA<NetworkingViewState.Success<User>>()
    verifyNoMoreInteractions(view)
  }
}


There is no validation around the User object being passed on to the success case. So how can we gain those formatting tests back?

view model meme

That’s right, view models! When we set the NetworkingViewState to Success in our presenter, rather than pass the User object we can pass a UserViewModel instead. And we can add tests for our UserViewModel in a separate class to ensure all values are formatted properly.

Something like this:

class UserViewModelTest {

  val mockUser = User("Amanda", "Hill", 100, "Aquarius")

  @Test
  fun testName() {
    val viewModel = UserViewModel(mockUser)
    val expected = "Amanda Hill"
    val actual = viewModel.fullName()

    assertEquals(expected, actual)
  }
}


Huzzah ! Test coverage regained! And blog post complete!

.Net developers: use Highcharts, the industry's leading interactive charting library, without writing a single line of JavaScript.

Topics:
android ,network ,mobile ,state machines

Published at DZone with permission of Amanda Hill, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}