Improving the Caching Game on Android in Kotlin

When we develop our applications we mostly have the best internet connections and we tend to not think about the number of requests the app will make to out back end server once it is live in production.

A good caching layer in our applications can vastly improve the user experience for people with not so good internet connections and at the same time take off some load of your server . It can also save you a couple of bucks when you are paying for cloud services.

What is caching ?

Caching is temporarily storing data in your application memory that you would normally fetch over the network (like an downloaded image from an URL) so that you can access it faster the next time it is required.
A couple of points to keep in mind when you are caching data :
  • A cache should not hog up the device memory. The cache architecture should be able to decide what data is needed and judiciously keep only the useful ones in memory.
  • A cache should not replace the main source . The stored data should be refreshed after a amount of time to make sure the data is fresh.
  • A cache layer should be abstracted from the main application logic. The application should only be concerned with handling the data and the cache should take care of refreshing , storing , deleting or fetching the data from the source.
Keeping all these factors in mind , I decided to create an open source caching library for android that will allow you to create a caching layer in your app in minutes. 😊
In this post I will show how you can cache responses that we can fetch from a server using a GET call. We will do this using just annotations to implement the cache layer . The entire source code for this example is available here.

Let's get coding

Add the following in your root buid.gradle file
allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}
Add the kotlin-kapt gradle plugin for annotation processing in your application buid.gradle file
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'
apply plugin: "kotlin-kapt"
Add the cold storage dependencies . Check the repository for the latest release and other features.
implementation "com.github.crypticminds.ColdStorage:coldstoragecache:2.0.1"
kapt "com.github.crypticminds.ColdStorage:coldstoragecompiler:2.0.1"
implementation "com.github.crypticminds.ColdStorage:coldstorageannotation:2.0.1"
Now we will write the logic for fetching data from the server.
For this example I am using “https://httpbin.org/get” ,which basically returns the parameters passed to the endpoint back to the caller. In our application we will just show the response received from the server.
import com.arcane.coldstorageannotation.Refrigerate
import java.net.URL

class MakeRemoteCall {

    /**
     * A method that makes a call to the "https://httpbin.org/get"
     * endpoint. This endpoint simply returns the arguments that were passed to it.
     * For the caching logic the parameter passed to the endpoint will act as
     * the key of the cache.
     *
     * Since there is only one parameter we don't need to specify the the keys
     * field in the annotation.
     */
    @Refrigerate(operation = "CallToServiceA")
    fun makeRemoteCallToServiceA(value: String): String {
        val url = "https://httpbin.org/get?param1=$value"
        val textResponse = URL(url).readText()
        return textResponse
    }


    /**
     * We will use the same endpoint but this time we will pass 3 parameters.
     * However , out cache key should only be the first 2.
     * We can configure the "keys" field in the annotation by specifying
     * the variable names that should act as the key of the cache.
     * In this case "parameter1" and "parameter2"
     */
    @Refrigerate(
        operation = "CallToServiceB", timeToLive = 10000,
        keys = ["parameter1", "parameter2"]
    )
    fun makeRemoteCallToServiceB(parameter1: String, parameter2: String, parameter3: String): String {
        val url = "https://httpbin.org/get?param1=$parameter1&param2=$parameter2&param3=$parameter3"
        val textResponse = URL(url).readText()
        return textResponse
    }
}
I am annotating the methods using the "@Refrigerate" annotation and passing the timeToLive , keys and operation parameters to it.
timeToLive : This attribute decides how long a data in the cache will remain "fresh" otherwise the cache will fetch it from the source instead of the cache.
keys : This parameter will accept the list of variables that the method takes as input , which will be considered as the keys of the cache. For example in remoteCallToServiceB function parameter1 and parameter2 will uniquely determine the result of the function and hence they will be considered the keys of the cache. If I pass "foo" and "bar" as the parameter values , the cache will store the corresponding result for this and in the next invocation of this function with "foo" and "bar" it will return the value from the cache.In case all the parameter will determine the result of a function , they keys attribute need not be specified.
operation : This is a mandatory attribute for the annotation. Operation will uniquely identify each method that has been annotated.
YOUR CACHE LAYER IS READY . (Yes it was this simple)

Under the hoods

When you annotate the methods using "@Refrigerate" it generated a new class called "GeneratedCacheLayer.kt" which will wrap all the functions with caching logic. Now instead of calling the functions in the MakeRemoteCall class you will be using GeneratedCacheLayer class to invoke the functions . Lets see how below :-
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.arcane.coldstoragecache.callback.OnOperationSuccessfulCallback
import com.arcane.coldstorageexamples.remotecall.MakeRemoteCall
import com.arcane.generated.GeneratedCacheLayer

class MainActivity : AppCompatActivity(), OnOperationSuccessfulCallback<String?> {

    /**
     * The make remote call object.
     *
     * @see MakeRemoteCall for details.
     */
    private val makeremoteCall = MakeRemoteCall()

    private lateinit var button: Button

    private lateinit var firstRemoteCall: TextView

    private lateinit var secondRemoteCall: TextView

    private val valueArray = arrayListOf("a", "b", "c")

    private var counter = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button = findViewById(R.id.button)
        firstRemoteCall = findViewById(R.id.remotecall1)
        secondRemoteCall = findViewById(R.id.remotecall2)


        /**
         * On button click random values from the array will be used to make the
         * remote calls.
         * The counter variable is used to control the two calls are made alternatively
         * on button clicks.
         */
        button.setOnClickListener {
            if (counter % 2 == 0) {
                GeneratedCacheLayer.makeRemoteCallToServiceA(
                    valueArray.random(),
                    makeremoteCall,
                    this
                )
            } else {
                GeneratedCacheLayer.makeRemoteCallToServiceB(
                    valueArray.random(),
                    valueArray.random(),
                    "constantvalue", makeremoteCall, this
                )
            }
            counter += 1
        }
    }

    /**
     * Populate the first text view with the response.
     */
    private fun populateFirstTextView(s: String) {
        runOnUiThread {
            firstRemoteCall.text = s
        }

    }

    /**
     * Populate the second text view with the response.
     */
    private fun populateSecondTextView(s: String) {
        runOnUiThread {
            secondRemoteCall.text = s
        }
    }

    /**
     * The callback implementation via which the result is
     * returned by the generated cache layer.
     */
    override fun onSuccess(output: String?, operation: String) {
        when (operation) {
            "CallToServiceA" -> populateFirstTextView(output!!)
            else ->            populateSecondTextView(output!!)
        }
    }
}
Import the file into the class where you want to invoke the function . In my case it is the main activity
import com.arcane.generated.GeneratedCacheLayer
Now call the function using this class
GeneratedCacheLayer.makeRemoteCallToServiceA(
                    valueArray.random(),
                    makeremoteCall,
                    this
                )
I am passing two new parameters in this generated function other than the ones that the original method already expects . One, is the instance of makeRemoteCall class since the generated function will use the original method to refresh the data in the cache and second, a call back interface (OnOperationSuccessfulCallback<String?> ) that I implemented and which will be used to pass the value to the main activity from the cache. This is required since all cache operations are performed asynchronously and the callback will be needed to pass the data to the UI thread.
To access this generated class normally like any other you can try running the app after applying all the annotations. Your IDE will automatically index the new class once it is generated.

Moment of truth

After you are done calling the functions using the generated class, run your app and check the logs. You will see logs like these in your logcat window
2020–01–14 02:26:45.132 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:46.004 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache miss due to stale data
2020–01–14 02:26:46.283 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Putting value in cache
2020–01–14 02:26:46.835 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:46.835 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:48.243 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Putting value in cache
2020–01–14 02:26:48.993 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:48.993 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:50.140 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Putting value in cache
2020–01–14 02:26:50.708 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit
2020–01–14 02:26:50.708 21851–21911/com.arcane.coldstorageexamples I/COLD_STORAGE: Cache hit

Your cache layer is fully functional now . 👍


Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!