The Cache library makes it easy to create an in-memory key/value cache that honors configured cache policies and optionally persists entries. It supports minimal configuration on Android by creating the cache file in the application's cache directory by default and also responds to low memory events.
In an application there is often a need to make data that is expensive to retrieve or create quickly accessible. The data may come from the network, a calculation, or something else. Along with making the data fast and easy to retrieve, policies may be needed to make sure the cached data does not grow unbounded and that the data does not go stale. It is possible the data should also be persisted locally so it is available between separate runs of the application. The primary purpose of this library is to make creating a cache that addresses these problems quick and easy with minimal configuration.
The core cache module contains an abstraction for serialization, and requires that an implementing module also be included, or a local implementation be created. Serialization implementations can be composed with the android module to provide the features of both. Currently, a kotlinx serialization implementation is provided.
The cache library modules are published on Maven Central and can be included as follows:
// core module
implementation("com.kroger.cache:cache:<version>")
// kotlinx serialization (core module is already included)
implementation("com.kroger.cache:kotlinx:<version>")
// if using android (core module is already included)
implementation("com.kroger.cache:android:<version>")Each serialization implementation comes with a default file persistence implementation
val fileCache = SnapshotFileCacheBuilder.from(
context, // application context used to reference application cache directory on Android
filename = "cacheFile.json", // cache file created in application's cache directory
KotlinCacheSerializer(serializer = CacheEntrySerializer(String.serializer(), Int.serializer())), // implementation of CacheSerializer
).build()Once the fileCache is created a MemoryCacheManager can then be created that utilizes it.
cache = MemoryCacheManagerBuilder.from(
context, // application context
fileCache, // persistent cache to periodically save cache entries to
)
.coroutineScope(coroutineScope) // CoroutineScope used to periodically save the cache
.build()Use the cache.
// add a value to the cache
cache.put("newKey", computeExpensiveValue())
// ...somewhere else retrieve and use the value
val expensiveValue = cache.get("newKey")
println(expensiveValue)Listed below are the key components of the cache library.
- SnapshotPersistentCache: This interface has two functions. One to read data from a persistent cache and another to save data to the persistent cache. Data is saved in "snapshots", meaning the persistent cache always reads and saves the full cached data at a specific point in time.
- CachePolicy: Used by the
MemoryCacheMangerto set properties such as max size and temporal policies (time to live and time to idle). - MemoryCacheManager: The
MemoryCacheManagerties together theCachePolicy,SnapshotPersistentCache(optional), and in-memory cache in a thread safe way.
The library provides an implementation of SnapshotPersistentCache that saves to a file. This can be used independently from the MemoryCacheManager if needed. Writing and reading to the file is not thread safe so if multiple threads use the same SnapshotPersistentCache make sure the access is synchronized. On Android the file is saved to the application's cache directory when using the Android builder factory function SnapshotFileCacheBuilder.from(...).
Each serialization implementation provides tooling to read and write data to file, via the CacheSerializer interface
Saves data to the file using a StringFormat from kotlinx.serialization defaulting to Json. A builder factory function overload is provided so a custom serialization strategy can be used. All that is required is
Note: Only one
SnapshotPersistentCacheshould exist at a time that references any given file.
A CachePolicy supports the following policies:
- Max Size: The maximum number of entries the
Cachecan store before removing the least recently used (LRU) entries. Defaults to-1meaning theCachecan grow unbounded. - Entry TTL: The time to live for an entry since it was first added to the
Cachebefore it will be removable from theCache. Defaults toDuration.INFINITE. - Entry TTI: The time to idle for an entry since it was last accessed. Retrieving an entry from the
Cachecounts as accessing it. Defaults toDuration.INFINITE.
Note: Only one temporal policy can be active at a time. Either Entry TTL or Entry TTI.
A CachePolicy can be created using a CachePolicy.builder().
val cachePolicy = CachePolicy.builder()
.maxSize(100) // maximum 100 entries allowed in cache
.entryTtl(24.hours) // entries become expired after 24 hours in the cache
.build()When creating a MemoryCacheManager the following properties can be configured:
- Coroutine Scope: The provided
CoroutineScopeis used to periodically update theSnapshotPersistentCacheif one is provided. It is important thisscopelives as long as theCacheis in use to make sure changes to thecacheare persisted. Once thescopeis cancelled a final save occurs. - Coroutine Dispatcher: The
CoroutineDispatcherto use when saving to the persistent cache if in use. Defaults toDispatchers.IO. - Snapshot Persistent Cache: An optional persistent storage to save the
cacheto. When set tonulltheMemoryCacheManageracts as an in-memorycacheonly that still enforces thecachePolicyset. - Save Frequency: How quickly a change to the
cachewill be saved to the persistent cache if in use. Defaults to 5 seconds. - Cache Policy: The cache policy the
MemoryCacheManagershould enforce. Defaults to a policy withmaxSize = 100andentryTtl = 24.hours. - Memory Level Notifier: An optional argument that allows the cache to respond to low memory and critical memory notifications. When using the
Androidbuilder this defaults to anAndroidMemoryLevelNotifierthat will remove expired entries from thecachewhen memory is low and will remove all entries from thecachewhen memory is critical according toComponentCallbacks2.onTrimMemory(level: Int). - Telemeter: The
telemeterto use for logging internal events and errors. Defaults to null (no logging).
A MemoryCacheManager can be created using one of the builder factory functions available. Below is a minimal example using the builder factory function for Android:
val cache = MemoryCacheManagerBuilder.from(
context, // application context
snapshotPersistentCache = fileCache, // see Cache Configuration section for an example
)
.coroutineScope(coroutineScope)
.build()When retrieving an entry from the Cache an expired entry will never be returned.