Architecture Components: Easy Mapping of Actions and UI State

When building an app, most of the time what we’re doing is pretty much mapping direct/indirect actions to some UI state.

While using Architecture Components, achieving this is quite easy with the help of LiveData + Coroutines + ViewModels — but it does require a bit of code to set it up.

Reason being that in order to observe “state” of a LiveData, we have to write a wrapper around its value, as well as integrate actions around this state.

Let’s take an example, there’s a list-based UI where

  • data is loaded from an API

Given these requirements, the actions would be:

Load

Swipe Refresh

Retry

And based on these actions, UI state can be one of these at any given time:

Success

Loading

Swipe-Refreshing

Failure

SwipeRefresh-Failure

Retrying

The State Machine

If we were to map the states and actions mentioned above using a diagram — it’ll look something like:

Actions can either be implicit or explicit. The difference is that explicit actions (shown as blue arrows) are those actions that are triggered by the user and implicit aren’t.

Let’s code it out!

State

Starting with the State, let’s create a wrapper for state representation using sealed classes.

Actions

Similar to the states, we’ll create a wrapper for representing actions

LiveData

We’ll need a custom LiveData that handles all actions and spits out appropriate state based on them (similar to what reducers do in Redux).

It should also do API call using Coroutines and propagate exceptions.

Implementation:

Using the new liveData block (that is actually a suspend block) and emit method — we can execute async code and emit values.

The switchMap block is also the new syntax for doing Transformations.switchMap() on a mutable LiveData.

Last part are the methods for dispatching actions.

ViewModel

As we’re dealing with Coroutines, we’ll specify the scope to be viewModelScope and use Dispatchers.IO as the coroutine context.

val users = ActionStateLiveData(viewModelScope.coroutineContext + Dispatchers.IO) {
userService.fetchUsers()
}

viewModelScope → Bind the lifetime of our Coroutine to the lifetime of ViewModel

Dipatchers.IO → Run coroutine block asynchronously

UI (Fragment/Activity)

Once all done — UI is pretty straight forward, we initialize the viewModel, dispatch the initial load action and observe result.

val viewModel: ProfileViewModel by viewModels()// In onCreate
viewModel.users.load()
swipeRefreshLayout.setOnRefreshListener {
viewModel.users.swipeRefresh()
}
retryButton.setOnClickListener {
viewModel.users.retry()
}
viewModel.users.state.observe(this) { state ->
when (state) {
Loading -> // show progress bar
Success -> // load up data
Failure -> // show error
Retrying -> // a different loader
SwipeRefreshing -> // show swipe refresh loader
SwipeRefreshingFailure -> // show error
}
}

That’s it for now — this was a basic example of how we can use LiveData + Coroutine + ViewModel to map actions & UI state. Things get a bit more tricky when dealing with pagination and unorthodox UI rules — I’ll try to cover those as well in the future.

Happy coding!

Web | Android | Senior Software Engineer @QuixelTools

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store