Android Repository Caching with MVVM and Clean Architecture

werner
5 min readDec 15, 2020

--

High Level View of Clean Architecture

Clean What?

Thanks to Uncle Bob —we have had a guidestone to modern “clean” software architecture for almost a decade now.

In the wild I have witnessed a convergence around this paradigm by developers using a diverse range of languages, not only in the compiled, type-safe world of C#, iOS and Android applications, but also in the web world, from React to Angular and all it’s cross platform children — even in Flutter.

The premise is not new — in fact, SOLID principles defined eons ago resemble what Uncle Bob is saying, however, he goes further by considering all that has changed since then (in our hyperconnected world being eaten by software).

Ok, on to Android (and the problem this post is about)…

Clean MVVM has become one of the standard architectures around which large Android projects are developed. The basic flow looks like this:

Why (Repository Caching)?

Imagine a shopping application where different view models in different parts of the application require some data from a repository — and let’s for example say that the repository returns a UserData object.

This UserData object is initially received from a RemoteDataSource (Backend API) and persisted (cached) on device when the user logs in.

In various view models, the fullName property from the UserData object is required — which is obviously not a frequently (if ever) updated value — to display the user’s name somewhere on their views.

For some business reason, the UserData object also contains a lastPurchaseDate property (which can be frequently updated on the backend), and somewhere in your app a view model requires this.

In this situation there needs to be a standard way for a view model or use case to tell the repository it needs a “fresh” copy of UserData — in other words, a repository needs a standard way of deciding whether to retrieve the requested data from the LocalDataSource or RemoteDataSource (and re-cache it to the LocalDataSource if necessary)

Proposal

The Cache Policy:

To solve this problem, I introduce the concept of a CachePolicy and (optionally) an associated CachePolicyRepository for repositories to extend.

The CachePolicy itself is a simple data class:

An instance of CachePolicy is created by the use case and passed to the repository when requesting data. In the context of our example, it may look something like this:

Note that here I am passing a userToken to key the cache with, which practically means there could be multiple users on this device and that their data is shared— so security considerations aside — this is just used as an example.

Here we are telling the repository: “Always return the cached copy of the data if you have it, otherwise fetch it, cache it, and return the cached copy”

Then, in the use case where we need the lastPurchaseDate property, we would call the repository with:

Here we are telling the repository to: “Fetch the data (whether already cached or not), upsert the cache line for it, and return it.”

Thus, the first use case is unaffected, as it will still always get the cached copy, and when it’s refreshed by another use case, it will get the “refreshed” copy next time it needs it.

Consequently, we can define more CachePolicy types for different requirements:

  • NEVER“Just fetch the latest data and return it (and don’t cache it)”
  • CLEAR “If you have a cache line for this key, return it, but also then delete it (so any future calls will be forced to fetch), otherwise just return the fetched copy as you will with the NEVER policy type”
  • EXPIRES“If you have a cache line for this key not older than the given timestamp, return it, otherwise, delete the entry and return (and cache) a fetched copy”

You can imagine the different use cases these different policy types will work for, but this only gets us partially there — i.e. Depending on project, you can use this as is, and apply the received CachePolicy in each repository as you see fit (i.e. managing the logic between data sources to realise the indicated CachePolicy.Type) — but in large projects, this is going against good SOLID principles.

The CachePolicyRepository:

If you have different datasource implementations in your project (without shared interface contracts), I suggest you sort it out first in order to implement the following.

I further propose a CachePolicyRepository for cache aware repositories to use. Here is a rudimentary example of such an implementation:

So given this, a basic repository can implement it as follows:

Notes and future improvements:

  • Although this only handles GET requests (by accepting just a url)—it can easily be modified to accept a Request object instead (perhaps for the RemoteDataSource to create a Retrofit instance with) — in which case you have to decide which property (or properties) in this object represents a unique value to key the cache with.
  • The LocalDataSource interface used here is suited for a Key/Value store — but can be refactored to handle relational data with, for example, Room (although you still need to nominate the DAO in the RemoteDataSource itself to stick to clean architecture)
  • The return type T here can optionally be enhanced by instead specifying a Response<T> — which will give you the ability to further refine this approach based on RemoteDataSource response codes / statuses when you would like to serve a cached copy of the data if the network call failed for some reason, for example, due to no connectivity / being offline.

Conclusion:

The main idea here is to provide flexibility to the caller as to how old the data requested from a repository should be.

From NEVER to ALWAYS and after some time with EXPIRED — I believe this approach will

  • Reduce redundant network calls.
  • Provide a basis to cater for situations when the device is offline.
  • Make repository caching policy an integral part of your architecture.

I hope you enjoyed this article, and I wish you all the best in your endeavours!

--

--