Android MVVM + Resource Call Status Observables ft. LiveData, RxJava, Room, ListAdapter, Dagger, and Kotlin

Recently, I had the opportunity to start a greenfield project for a client with a team of colleagues. The team was given a couple weeks to architect a robust foundation for the Android app. Below are a few goals I wanted to keep in mind as we chatted about how the codebase should look.

  1. The team will be handing the project off to the client after our contract, so I want to ensure that the code is as scalable, intuitive, and self-documenting as possible. This means high traceability, high maintainability, organized packaged structures, and meaningful and concisely-named classes, functions, and variables.
  2. The client has a requirement for high test coverage, so I want to ensure high testability to make testing as painless as possible. This means proper encapsulation, loose coupling, high cohesion, and low redundancy.
  3. There are a handful of things that happen while making a request to get data and a handful of things that can happen as a result of that request. I want to make it easy for the UI to do its job of properly informing the user of what’s happening and why the user may not be seeing what’s expected. This means knowing when and when not to show ProgressBars, empty states, error states, Toasts, Snackbars, and other similar indicators.

After some debates and tweaks, we landed on a variant of MVVM that was fundamentally influenced by Android’s guide to app architecture.

High-level architecture diagram

PersistableNetworkResourceCall

The order of the data flows in the repository layer is coordinated by a PersistableNetworkResourceCall, or PNRC for short.

PersistableNetworkResourceCall

Note that ResponseType is the data type that you expect from the success payload, the DTO, of the network call, and the ResourceType is the data type of your local model that you plan to persist locally and emit to any observers such as ViewModels. Your ResponseType and ResourceType might be the same, but this might not always be the case.

The implementation details of retrieving the data and handling the success callback of the network call are handled by a repository class.

JobRepository (see demo for context)

Check out NeworkResourceCall for a call flow that doesn’t need to load from a local database, such as a login request.

Demo

The simplest way to explain the architecture is by example, so here’s a demo app that demonstrates an interaction flow of navigating to a screen and loading a list of items. In this particular example, items are represented by Job entities. Below is the flow of how a list of Jobs is loaded and displayed on the screen and what happens if Jobs aren’t found or the request to retrieve them fails. Follow along in the code for full context around each step.

  1. The user launches the app. JobsActivity is created.
  2. Within JobsActivity.onCreate(), JobsViewModel is initialized; the JobsViewModel's loading, error, noJobsFound, and presentation LiveDatas are observed; and JobsViewModel.bind() is called.
  3. The call to JobsViewModel.bind() causes JobsViewModel to subscribe to JobRepository.getJobs().
  4. JobRepository.getJobs() instantiates a PNRC. The PNRC creates anObservable¹, emits a Loading status with null data to its observer(s): JobsViewModel, and attempts to load the Jobs from the Room database. Since there is no data to show in the initial Loading emission, JobsViewModel just posts true to its loading SingleLiveEvent, which tells JobsActivity to show its ProgressBar.
  5. When the database query completes, the PNRC emits another Loading status with the data returned from the database query followed by a call to the web API to attempt to fetch fresh data from the network. If the Loading emission’s data from the database query is not null, JobsViewModel builds a Presentation object from the JobWithRelations List and posts the Presentation to the presentation LiveData. Within the initialization of the Presentation, the Jobs are adapted to UI-consumable properties that get represented within Presentation.Model objects.² When JobsActivity receives the list of Presentation.Models, JobsActivity submits the list to JobsAdapter to adapt the view data to a list of CardViews.
  6. When the network request completes, the PNRC emits one of three statuses: ResourceFound with non-null data if the request was successful (2xx HTTP status code), and the response contains a resource that isn’t null or empty (7.); NoResourceFound with null data if the request was successful, but the resource is null or empty (8.); or Error with a custom data.core.Error object if the request failed with a non-2xx HTTP status code (9.).
  7. If the status is ResourceFound, JobsRepository converts the JobResponse DTOs to Job and Worker entities, interfaces with the appropriate DAOs to insert or update the Jobs and Workers into the database, streams the JobDao to listen for any changes made to active Jobs or their Workers in the database, and emits a ResourceFound status with the data from the result of the stream query.³ JobsViewModel builds a Presentation object from the JobWithRelations List and posts the Presentation to the presentation LiveData. JobsActivity submits the list to JobsAdapter.
  8. If the status is NoResourceFound, JobsRepository emits a NoResourceFound status, JobsViewModel notifies its noJobsFound LiveData, and JobsActivity hides the RecyclerView and shows the empty state Views to inform the user that no active jobs were found.
  9. If the status is Error, PNRC emits an Error status with a custom data.core.Error that contains an appropriate error icon, title, and description; JobsViewModel posts the Error to the error LiveData; and JobsActivity shows an error state with the given error icon, title, and description to notify the user that the request failed in the case that no active Jobs were found in Room.
Job Tracker demo app: active jobs list with an error state (left) and with jobs to show (right)
¹ We decided to make use of Rx Observables in our repository and data source layers rather than LiveData since these layers don’t need to be lifecycle-aware, and Observables provide more features for transforming and preparing data.
² Presentation.Model objects are passed to the Activity rather than full Job objects so that the UI doesn’t need to know about Jobs or how to translate them. This allows for relatively light and dumbActivity and Fragment classes.
³ The Jobs are stored in Room and emitted from Room rather than emitting the network response directly. This assigns Room as the single source of truth.

This is my first stab at using MVVM in this way, so I’m looking forward to seeing how it works out for my team as product requirements grow and change. One of the main reasons I’m sharing this demo here is to get your feedback. I’m rarely convinced that an architecture is ever finished or complete, so drop a line if you have any thoughts, concerns, or questions. ✌️