Article

Stop Putting State In Your View Models

Collin Flynn

March 13, 2018

By Gabriel Tocu (Own work) CC BY-SA 4.0 , via Wikimedia Commons

One of the benefits of separating state from your decision-making code is that the decisions become reproducible.

You don’t want your decision-making functions to change their moment-to-moment behavior based on a history of events preceding them. That increases branching and multiplies the possible causes of misbehavior. Was a bug caused by bad state, bad logic, or a combination of the two?

If you’re not paying attention, it’s easy to let state creep into your view models and pollute your logic.

Consider an example.

Application State Inside A View Model

Imagine an app with a simple file download feature.

  1. Check if there is a file to download. If so, show a download button.
  2. When the download button is clicked, start the download.
An example UI for downloading a file

See the state? Here it is again, but this time from an API interface:

And the relevant models:

Model classes returned from the DownloadApi

Where is the state? Between the two service calls! DownloadApi.search() and DownloadApi.downloadFile().

The UI flow steps can now be specified further (state included):

  1. Call DownloadApi.search() and see if the returned FileSearchResult has a file to download.
  2. If no file exists, show the empty UI.
  3. If there is a file to download, remember the file ID
  4. When the download button is clicked, pass the file ID to the DownloadApi.downloadFile() to start the download.

State itself is not a problem, but put in the wrong place (or multiple places at once) results in unstated possibilities that you’ll never explicitly model. If you’ve ever written something like this,

else {
// This will never happen.
}

Then you might have failed to model all the real states, or you introduced implicit states by distributing it to multiple owners.

A (Bad) View Model

A naive place to store the RemoteFile search result would be inside the View Model, just after it’s returned from DownloadApi.search().

A View Model that tries to manage state, rather than respond to it. Though this example is somewhat trivial, notice that the complexity would increase quickly if there were more service APIs or UI events involved.

The internal state of this View Model can alter its outward behavior by the absence or presence of the nullableRemoteFile. Even worse, the startDownload() method is forced to deal with a poorly modeled possibility: somehow the internal state is bad, and there’s not an explicit reason.

Is this a logic error or a problem with the data layer? Is there a possibility of recovery, and if so how? And in the meantime, what do you tell the user?

Delegate State, Don’t Manage It

In the View Model, the startDownload() method can be re-written:

With the RemoteFile? member field gone, the startDownload() method needn’t worry about a nullable internal state. It’s passed in directly when the View Model needs to react to an event.

Who passes in the RemoteFile? There are multiple viable options that I will mention shortly, but now that the state is pulled from the View Model, you can start to think about who really owns it. You can also understand and explicitly model the errors that before were implicit and poorly understood.

Where does the state go?

There’s two adjacent layers to the View Model:

  1. The View Layer (suitable for transient state)
  2. The Data Layer (suitable for persistent state)

In many circumstances it’s not wise to place much (or any) state inside the view layer. However, consider the nature of that state you’d like to save.

  • Is it safe to store this state as a temporary interaction for the user? Think EditText , CheckBox, or other UI elements that may need to be refreshed when the UI is recreated.
  • Is it safe to lose this state? Does it represent something that should out-live the UI, or can it be recovered easily from source when new UI is created?

The Data Layer could explicitly write down anything you care about such that a View Model observing it can push new states to the UI, accurately reflecting whatever you need to persist. An example might be a file that multiple users can remotely update, or the state of a background download that doesn’t correspond to any UI. The presence or absence of a RemoteFile can also be modeled in the data layer!

Alternatively, in the DownloadViewModel example, a temporary UI element could reasonably hold state if we don’t mind losing it when the UI is destroyed.

Carefully consider where state should reside. Is it transient, or should it be persisted?

Does putting state into the View Layer seem gross to you? Or can you imagine cases where you’d be perfectly comfortable delegating state to the UI in this manner?

Either way, good! Acknowledging the state’s existence means you can decide how to model it. Now you can confront each state explicitly, whereas before the nullable member field in the View Model was pushing complexity under the rug, only to pop out later as an error condition without an explanation.

Keep your decisions stateless, and your state decisionless.

Collin handles states at Livefront , and he thanks you for reading.