AndroidBites | 5 Steps to LRU Cache in Repository.

android Sep 07, 2020

No more wasting APIs calls on restoring your instance state.

AndroidBites | How to Implement LRU in repository in 5 easy steps 


Not a promotion but my bud wiseAss aka Ryan Kay, is trying hard to share lots of information about software engineering specially related to Android and Kotlin, please do support him by purchasing his learning material , if you can't please do share these links, and Β his youtube channel for awesome free content.

πŸ™‡πŸ»β€β™‚οΈ Thanks a lot! Follow Ryan's Youtube. Please continue without disturbance...


There are many variations of Repository pattern just like any other Architectural Pattern and can be bend according to the needs of the Software, but the core responsibility of Repository is to provide a clean API so that the rest of the app can retrieve it’s data easily.

They consume various data sources from the app to get the data. These data sources could be local models, web services, and caches.

Today’s article is an example of how you can implement LRU caching into the repository so you can save some API/Database calls.

Step 1 : Create a Tracker for the source of the data. Β πŸ‘©β€πŸ’»

We need to have a field that can be used to figure out from which origin does the data source is being fetched from, for that you can use an Enum class.


// creating a enum for source of data
enum class SOURCE {
    CACHE,
    LOCAL,
    REMOTE
}

Kotlin | Enum for tracking data source

Step 2 : Decide the Type to be Cached. πŸ€”

Since cache stores multiple type of data, storing Any in the cache could be a good option but can we do better?, Yes by using a marker interface, that could help in designing a contact which could store meta-information related to cached data entry, for example data source which we have created in step 1 and something like a expire time/stale time which can be used to invalidate data in cache.


// marker interface for cachable type
interface Cacheable {
	
    // track the data origin
    val source: SOURCE

	// cache create time
    val createAt: Long  
    
    // expire time after which data is considered invalid
    val staleTime: Long 
   	
    // function which compute if the data has expire or not
    fun isExpired(): Boolean
}

Kotlin | Cacheable marker interface

Step 3 : Implementing LRU Cache using LinkedHashMap 🀩

LinkedHashMap is a blend of a linked list and a hash map. By default when you iterate over it, you get back the insertion order. This is useful when you want to build up a dictionary of entries that should be fast to access, but also have a very specific order.

One of the constructors is used to create this type of LinkedHashMap: LinkedHashMap(initialCapacity, loadFactor, true) - the true means "accessOrder" instead of the default, which is "insertionOrder" is preserved.

Using this feature of LinkedHashMap is the ability to also retrieve elements in the order in which they were last accessed.

Another factor that is very special is that we can also automatically remove the eldest entry if we have exceeded our maximum number. To do that we subclass LinkedHashMap and add a maxEntries field. We also override the removeEldestEntry(Map.Entry<K, V> eldest) method from LinkedHashMap to return true when we have reached our maximum size.

These features make it a perfect candidate to behave as a LRU cache.


// LinkedHashMap as LRU cache
class LRUCache(private val cacheSize: Int) :
    LinkedHashMap<String, Cacheable>(
          /*initSize*/cacheSize, 
          /*loadFactor*/0.75f,
          /*accessOrder*/ true) {
  override fun removeEldestEntry(
          eldest: MutableMap.MutableEntry<String, Cacheable>?
  ): Boolean {
        return this.size > cacheSize
  }
}

Kotlin | LinkedHashmap as LRU cache

Step 4 : Skeletal Class for BaseRepository πŸ’»

You can create a Skeletal class/baseClass that can abstract away repetitive code of creating LRU cache and common functions to get entry from cache.


// Skeletal class for cache repository
abstract class BaseRepository {
    
    // creating a object of LRU cache
    private val cache = LRUCache(cacheSize)
    
    // cacheSize, is abstract so that 
    // each reposity can decide its buffer size
    abstract val cacheSize: Int

    // read value from cache, return null when
    // 1. item is not in cache
    // 2. item is in cache but has expired
    @Suppress("UNCHECKED_CAST")
    fun <T> fromCache(key: String): T? {
        val cacheValue = cache.get(key)
        if (cacheValue?.isExpired() == true) {
            return null
        }
        return cacheValue as? T
    }

    // put value into the cache
    fun setCache(key: String, value: Cacheable) {
        cache.put(key, value)
    }
}

Kotlin | Base Repository

Step 5 : Creating Cacheable DTOs πŸ“

Your data classes need to extend Cacheable interface , so that they could be compatible with the cache, and we can associate functionality that decide data-source origin and add logic that defines when the item is invalidated into the cache.


// marking data with cacheable interface
data class Person(
    val name: String,
    
    // set default source of data
    override val source: SOURCE = SOURCE.REMOTE,  
    
    // cache creation time
    override val createAt: Long = DateTime.now().millis, 
    
    // max data cache life
    override var staleTime: Long = 500 
    
) : Cacheable {
    
    // tip : you can even shift this function to interface
    // if you like to keep the functionality same.
    override fun isExpired(): Boolean {
        // do stuff... that decide data is invalidated/expired
        val currentTime = DateTime.now().millis
        val isExpired = currentTime - createAt >= staleTime
        return isExpired
    }
    
}

Kotlin | Data class as Cacheable

Behavioural Example. πŸ‘·πŸ»β€β™€οΈ

Implementation example :


class PersonRepository : BaseRepository() {

    override val cacheSize: Int
        get() = 3
    
    // get from cache if valid or from local source
    fun getCacheOrPerson(name: String): Person {
    	// get from cache
        val cachedPerson = fromCache<Person>(name) 
        return when {
            cachedPerson != null -> cachedPerson
            else -> createPerson(name)
        }
    }
  
    // create data locally and cache
    fun createPerson(name: String): Person {
        return Person(name, source = SOURCE.LOCAL).also {
        	// copy into cache
            setCache(name, it.copy(source = SOURCE.CACHE)) 
        }
    }
}

Kotlin | Example Repository

Cache storing the value :


 val repo = PersonRepository();
 repo.fromCache<Person>("ch8n").also { println(it)} 
 // output : null
 --------------------------------------------------------
 val repo = PersonRepository();
 repo.createPerson("ch8n")
 repo.fromCache<Person>("ch8n").also { println(it)} 
 // output : Person(name=ch8n, source=LOCAL, createAt=1599314478242, staleTime=500)

Kotlin | sample getting data

Cache expiring value after stale time passed :


 val repo = PersonRepository();
 repo.fromCache<Person>("ch8n").also {
    println(it)
 }
 Thread.sleep(400)
 repo.fromCache<Person>("ch8n").also {
    println(it)
 }
 Thread.sleep(400)
 repo.fromCache<Person>("ch8n").also {
    println(it)
 }
 // output :
 // Person(name=ch8n, source=CACHE, createAt=1599315201644, staleTime=500)
 // Person(name=ch8n, source=CACHE, createAt=1599315201644, staleTime=500)
 // null
 
Kotlin | sample data expire after some time

Create object if not present in cache and then cache :


 val repo = PersonRepository();
 repo.getCacheOrPerson("ch8n").also {
    println(it)
 }
 repo.fromCache<Person>("ch8n").also {
    println(it)
 }
 // output
 // Person(name=ch8n, source=LOCAL, createAt=1599315492835, staleTime=500)
 // Person(name=ch8n, source=CACHE, createAt=1599315492835, staleTime=500)
 
Kotlin | sample create data/ call api when cache not have entry

Read from cache


 val repo = Repository()
 repo.getCacheOrPerson("ch8n").also { println(it) }
 repo.getCacheOrPerson("chetan").also { println(it) }
 repo.getCacheOrPerson("ch810").also { println(it) }

 repo.getCacheOrPerson("ch8n").also { println(it) }
 repo.getCacheOrPerson("chetan").also { println(it) }
 repo.getCacheOrPerson("ch810").also { println(it) }

 // output
 // Person(name=ch8n, source=LOCAL, createAt=1599315961811, staleTime=500)
 // Person(name=chetan, source=LOCAL, createAt=1599315961853, staleTime=500)
 // Person(name=ch810, source=LOCAL, createAt=1599315961853, staleTime=500)
 // Person(name=ch8n, source=CACHE, createAt=1599315961811, staleTime=500)
 // Person(name=chetan, source=CACHE, createAt=1599315961853, staleTime=500)
 // Person(name=ch810, source=CACHE, createAt=1599315961853, staleTime=500)
 
Kotlin | reading from cache 

If value is added to filled cache


 val repo = Repository()
 repo.getCacheOrPerson("ch8n").also { println(it) }
 repo.getCacheOrPerson("chetan").also { println(it) }
 repo.getCacheOrPerson("ch810").also { println(it) }
 repo.getCacheOrPerson("che10").also { println(it) }

 repo.getCacheOrPerson("ch8n").also { println(it) }
 
 // Person(name=ch8n, source=LOCAL, createAt=1599316273944, staleTime=500)
 // Person(name=chetan, source=LOCAL, createAt=1599316273985, staleTime=500)
 // Person(name=ch810, source=LOCAL, createAt=1599316273985, staleTime=500)
 // Person(name=che10, source=LOCAL, createAt=1599316273985, staleTime=500)
 // Person(name=ch8n, source=LOCAL, createAt=1599316273985, staleTime=500) 

  // since ch8n is added much later in the cache when new value che10 
  // entered the cache, ch8n is removed from cache and che10 is added 
  // hence when again ch8n was called from function if was created again 
  // and cached removing chetan from the cache.
  
Kotlin | Reading from cache on overflow
Update#1 [7 Sept 2020] : From Reddit user name Volt316, pointed me out that android officially have LRU cache file pre implemented similar to what I did, you can checkout from here , also a DiskCache for which you can checkout from here and here (I will do a simplified version for it in future). Thanks Volt316!

Conclusion πŸ’†πŸ»β€β™€οΈ

From the above example, we have successfully implemented a LRU cache in our repository. It's a very easy example you can mold it according to your requirements.

Hope you find it informative and if you have any feedback or post request or want to subscribe to my mailing list forms are below.

Until next time. Happy Hacking! πŸ‘©β€πŸ’»


Enjoy the article?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)




Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.