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.
- 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.
- 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.
- 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.


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


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.


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.
- The user launches the app.
JobsActivityis created. - Within
JobsActivity.onCreate(),JobsViewModelis initialized; theJobsViewModel'sloading,error,noJobsFound, andpresentationLiveDatas are observed; andJobsViewModel.bind()is called. - The call to
JobsViewModel.bind()causesJobsViewModelto subscribe toJobRepository.getJobs(). JobRepository.getJobs()instantiates a PNRC. The PNRC creates anObservable¹, emits aLoadingstatus withnulldata to its observer(s):JobsViewModel, and attempts to load theJobs from theRoomdatabase. Since there is no data to show in the initialLoadingemission,JobsViewModeljust poststrueto itsloadingSingleLiveEvent, which tellsJobsActivityto show itsProgressBar.- When the database query completes, the PNRC emits another
Loadingstatus 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 theLoadingemission’sdatafrom the database query is not null,JobsViewModelbuilds aPresentationobject from theJobWithRelationsListand posts thePresentationto thepresentationLiveData. Within the initialization of thePresentation, theJobs are adapted to UI-consumable properties that get represented withinPresentation.Modelobjects.² WhenJobsActivityreceives the list ofPresentation.Models,JobsActivitysubmits the list toJobsAdapterto adapt the view data to a list ofCardViews. - When the network request completes, the PNRC emits one of three statuses:
ResourceFoundwith non-nulldata if the request was successful (2xx HTTP status code), and the response contains a resource that isn’t null or empty (7.);NoResourceFoundwithnulldata if the request was successful, but the resource is null or empty (8.); orErrorwith a customdata.core.Errorobject if the request failed with a non-2xx HTTP status code (9.). - If the status is
ResourceFound,JobsRepositoryconverts theJobResponseDTOs toJobandWorkerentities, interfaces with the appropriate DAOs to insert or update theJobs andWorkers into the database, streams theJobDaoto listen for any changes made to activeJobs or theirWorkers in the database, and emits aResourceFoundstatus with the data from the result of the stream query.³JobsViewModelbuilds aPresentationobject from theJobWithRelationsListand posts thePresentationto thepresentationLiveData.JobsActivitysubmits the list toJobsAdapter. - If the status is
NoResourceFound,JobsRepositoryemits aNoResourceFoundstatus,JobsViewModelnotifies itsnoJobsFoundLiveData, andJobsActivityhides theRecyclerViewand shows the empty stateViews to inform the user that no active jobs were found. - If the status is
Error, PNRC emits anErrorstatus with a customdata.core.Errorthat contains an appropriate error icon, title, and description;JobsViewModelposts theErrorto theerrorLiveData; andJobsActivityshows an error state with the given error icon, title, and description to notify the user that the request failed in the case that no activeJobs were found in Room.




¹ We decided to make use ofRxObservablesin our repository and data source layers rather thanLiveDatasince these layers don’t need to be lifecycle-aware, andObservablesprovide more features for transforming and preparing data.
²Presentation.Modelobjects are passed to theActivityrather than fullJobobjects so that the UI doesn’t need to know aboutJobs or how to translate them. This allows for relatively light and dumbActivityandFragmentclasses.
³ TheJobs are stored inRoomand emitted fromRoomrather than emitting the network response directly. This assignsRoomas 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. ✌️