If there is one performance optimization you should know, it is certainly caching. It is used in many different ways on many different levels. Your computer has a CPU cache and a disk cache. Your browser has a cache for web pages. Our network providers have caches for different types of data. JVM has many different caches that are used to speed up our applications. We define our own caches in both backend and Android applications. Caching is everywhere because it’s a very powerful technique. I would even say that caching is the most powerful way to speed up applications.
The idea behind caching is simple. A cache contains a redundant copy of data that is stored such that we can access it quickly. Let me show you a typical example. Imagine that your application needs to fetch users by their id from a web service. You can implement this in the following way:
class WebUserRepository(
val userClient: UserClient
) : UserRepository {
override suspend fun getUser(id: Int): User =
userClient.fetchUser(id)
}
The problem is that for each user we want to get, we need to send a network request and wait for the response. One way to improve this is by storing the results of previous requests so we don't need to send a network request every time we want to get a user with a certain id. We can do this using a map:
class CachedWebUserRepository(
val userClient: UserClient
) : UserRepository {
private val users = ConcurrentHashMap<Int, User>()
override suspend fun getUser(id: Int): User =
users.getOrPut(id) { userClient.fetchUser(id) }
}
This is a simple implementation of a cache. Our map represents redundant memory because if we cleared it, no data would really be lost and our repository would just need to fetch it again. We can also see that we can access data from a cache quickly as we don't need to send a network request. However, there are two problems with caches. Firstly, if we cache data that changes, our cache can become stale. For instance, if we cache users and a user’s name changes, the data in our cache will be outdated. The standard solution to this problem is to use a caching library (like Caffeine or Ehcache) that allows us to specify how long a certain record should be considered valid. For instance, you can specify that our user record should be considered valid for one minute. After that, we will need to fetch it again. Such a solution makes a lot of sense on backend applications where users often fetch the same data repeatedly but the data rarely change. This is a typical example of using a
cache on the backend:
class CachedWebUserRepository(
val userClient: UserClient
) : UserRepository {
private val users = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildSuspending<Int, User>()
override suspend fun getUser(id: Int): User =
users.get(id) { userClient.fetchUser(id) }
}
Using time-based expiration is far less popular on Android, where we tend to use caching for data that does not change, like configurations or database connections, or data that are only changed in an application, where we can update cached objects when they change.
private val connections =
ConcurrentHashMap<String, Connection>()
fun getConnection(host: String) =
connections.getOrPut(host) { createConnection(host) }
The second problem with caching is that it essentially means we are buying performance in exchange for memory. If we cache too much data, we can run out of memory, but there are a couple of tricks that can help us with this problem. One is to expire cache entries that are used less often (expire after write) or to set a cache size limit. However, the most powerful technique is making your cache use soft or even weak references. Let me explain this.
In Kotlin, when a variable references a value, it is a strong reference, so the existence of this reference prevents the garbage collector from cleaning up the value. However, JVM also offers two other kinds of references:
A weak reference does not prevent the Garbage Collector from cleaning up a value. So, if no other reference is using this value, it will be cleaned up.
A soft reference does not guarantee that a value won’t be cleaned up by the GC either, but in most JVM implementations this value won’t be cleaned up unless memory is needed.
If you are concerned about memory usage, the simplest way is to use a soft reference cache as this will not be limited when there is enough memory, but it will be cleaned up when memory is needed.
class CachedWebUserRepository(
val userClient: UserClient
) : UserRepository {
private val users = Caffeine.newBuilder()
.maximumSize(10_000)
// When size is reached, less used entries are removed
.expireAfterAccess(10, TimeUnit.MINUTES)
//When entry is not used for 10 minutes, it is removed
.softValues() // Using soft references
.buildSuspending<Int, User>()
override suspend fun getUser(id: Int): User =
users.get(id) { userClient.fetchUser(id) }
}
Summary
Use caching to speed up data access and reduce the number of heavy requests (like web and file system requests).
Use a caching library to avoid common pitfalls and to get more features.
Use time-based expiration to avoid stale data and to limit the size of the cache.
Use a cache size limit and soft references to avoid running out of memory.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.
Emanuele is passionate about Android and has been fascinated by it since 2010: the more he learns, the more he wishes to share what he knows with others, which is why he started maintaining his own blog.
In his current role as Senior Android Developer at Mozio, he is now focusing on Kotlin Multiplatform Mobile: he has already given a couple of talks on this topic on various occasions, so far.