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 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
RemoteDataSource (and re-cache it to the
LocalDataSource if necessary)
The Cache Policy:
To solve this problem, I introduce the concept of a
CachePolicy and (optionally) an associated
CachePolicyRepository for repositories to extend.
CachePolicy itself is a simple
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
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.
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
GETrequests (by accepting just a
url)—it can easily be modified to accept a
Requestobject instead (perhaps for the
RemoteDataSourceto 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.
LocalDataSourceinterface 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
RemoteDataSourceitself to stick to clean architecture)
- The return type
There can optionally be enhanced by instead specifying a
Response<T>— which will give you the ability to further refine this approach based on
RemoteDataSourceresponse 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.
The main idea here is to provide flexibility to the caller as to how old the data requested from a repository should be.
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!