Kache

Cache LibraryKotlinMultiplatformAndroidiOSMemory CacheFile Cache

Cache Library

Kache

Overview

Kache is a lightweight Kotlin Multiplatform caching library that supports both in-memory and file-based persistent caches, offering multiple eviction strategies (LRU, FIFO, MRU, and FILO).

Details

Kache is a Kotlin Multiplatform caching library developed by MayakaApps. It provides a unified caching solution for cross-platform development by allowing the use of the same API across Android, iOS, and desktop applications. It supports both in-memory and file-based persistent caches, offering four eviction strategies: LRU (Least Recently Used), FIFO (First In First Out), MRU (Most Recently Used), and FILO (First In Last Out). It's fully compatible with Kotlin Coroutines for non-blocking asynchronous processing. The type-safe API design leverages compile-time type checking. For file caching, it provides automatic file management and cleanup functionality, with configurable memory usage limits and cache size settings. Despite its simple and intuitive API, it balances high performance with flexibility.

Pros and Cons

Pros

  • Multiplatform Support: Same API available for Android, iOS, and desktop
  • Rich Eviction Strategies: Supports four strategies - LRU, FIFO, MRU, FILO
  • Memory & File Caching: Choose optimal cache type based on use case
  • Coroutines Support: Non-blocking asynchronous processing
  • Type Safe: Design leveraging Kotlin's type safety
  • Lightweight Design: Minimal dependencies and overhead
  • Auto Management: Automatic cleanup functionality for file cache

Cons

  • New Library: Relatively new with limited large-scale track record
  • Kotlin Only: Cannot be used with languages other than Kotlin
  • Limited Documentation: Detailed documentation and samples are limited
  • Ecosystem: Fewer surrounding tools compared to other cache libraries
  • Limited Advanced Features: No support for advanced features like Redis Pub/Sub, Cluster, etc.

Official Links

Code Examples

Gradle Dependencies Setup

// build.gradle.kts (common module)
dependencies {
    // In-memory cache
    implementation("com.mayakapps.kache:kache:2.1.0")
    
    // File-based cache
    implementation("com.mayakapps.kache:file-kache:2.1.0")
}

Basic Memory Cache Operations

import com.mayakapps.kache.Kache
import com.mayakapps.kache.KacheStrategy

// Memory cache initialization
val cache = Kache<String, ByteArray>(
    maxSize = 5 * 1024 * 1024 // 5MB
) {
    strategy = KacheStrategy.LRU
}

// Data storage and retrieval
suspend fun cacheExample() {
    val key = "user_profile_123"
    
    // Get data from cache, create new if not exists
    val userData = cache.getOrPut(key) {
        // Heavy processing (API calls, database access, etc.)
        fetchUserDataFromApi(key)
    }
    
    // Direct value setting
    cache.put("simple_key", "simple_value".toByteArray())
    
    // Value retrieval
    val cachedValue = cache.get("simple_key")
    println("Cached value: ${cachedValue?.let { String(it) }}")
    
    // Check cache size and hit ratio
    println("Cache size: ${cache.size}")
    println("Hit ratio: ${cache.hitRatio}")
}

File-based Cache Operations

import com.mayakapps.kache.FileKache
import com.mayakapps.kache.KacheStrategy
import java.io.File

// File cache initialization
val fileCache = FileKache(
    directory = File("cache"),
    maxSize = 50 * 1024 * 1024 // 50MB
) {
    strategy = KacheStrategy.LRU
}

suspend fun fileCacheExample() {
    val imageUrl = "https://example.com/image.jpg"
    val cacheKey = "image_${imageUrl.hashCode()}"
    
    try {
        // Get data from file cache, download if not exists
        val imageFile = fileCache.getOrPut(cacheKey) { tempFile ->
            try {
                // Download file and save to temp file
                downloadImageToFile(imageUrl, tempFile)
                true // Return true on success
            } catch (e: Exception) {
                false // Return false on failure (file will be auto-deleted)
            }
        }
        
        if (imageFile != null) {
            println("Image cached at: ${imageFile.absolutePath}")
            // Use the file
            displayImage(imageFile)
        }
    } finally {
        // Resource cleanup
        fileCache.close()
    }
}

Using Different Eviction Strategies

import com.mayakapps.kache.Kache
import com.mayakapps.kache.KacheStrategy

class CacheExamples {
    
    // LRU (Least Recently Used) - Remove least frequently used items
    private val lruCache = Kache<String, UserData>(maxSize = 100) {
        strategy = KacheStrategy.LRU
    }
    
    // FIFO (First In First Out) - Remove oldest items
    private val fifoCache = Kache<String, ProductData>(maxSize = 200) {
        strategy = KacheStrategy.FIFO
    }
    
    // MRU (Most Recently Used) - Remove most frequently used items
    private val mruCache = Kache<String, TempData>(maxSize = 50) {
        strategy = KacheStrategy.MRU
    }
    
    // FILO (First In Last Out) - Remove newest items
    private val filoCache = Kache<String, LogData>(maxSize = 300) {
        strategy = KacheStrategy.FILO
    }
    
    suspend fun demonstrateStrategies() {
        // LRU cache usage example
        val user = lruCache.getOrPut("user_123") {
            UserData("John", "[email protected]")
        }
        
        // FIFO cache usage example
        fifoCache.put("product_456", ProductData("Laptop", 999.99))
        
        // Display cache statistics
        println("LRU Cache - Size: ${lruCache.size}, Hit Ratio: ${lruCache.hitRatio}")
        println("FIFO Cache - Size: ${fifoCache.size}, Hit Ratio: ${fifoCache.hitRatio}")
    }
}

data class UserData(val name: String, val email: String)
data class ProductData(val name: String, val price: Double)
data class TempData(val value: String)
data class LogData(val message: String, val timestamp: Long)

Android-specific Image Cache Implementation

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.mayakapps.kache.Kache
import com.mayakapps.kache.FileKache
import com.mayakapps.kache.KacheStrategy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.net.URL

class ImageCacheManager(private val context: Context) {
    
    // Memory cache (for decoded Bitmaps)
    private val memoryCache = Kache<String, Bitmap>(
        maxSize = 20 * 1024 * 1024 // 20MB
    ) {
        strategy = KacheStrategy.LRU
    }
    
    // File cache (for original image data)
    private val fileCache = FileKache(
        directory = File(context.cacheDir, "images"),
        maxSize = 100 * 1024 * 1024 // 100MB
    ) {
        strategy = KacheStrategy.LRU
    }
    
    suspend fun loadImage(url: String, maxWidth: Int, maxHeight: Int): Bitmap? {
        val cacheKey = "${url}_${maxWidth}x${maxHeight}"
        
        // First check memory cache
        memoryCache.get(cacheKey)?.let { return it }
        
        // Get image from file cache or download
        val imageFile = fileCache.getOrPut(url) { tempFile ->
            try {
                downloadImage(url, tempFile)
                true
            } catch (e: Exception) {
                false
            }
        }
        
        return imageFile?.let { file ->
            // Decode bitmap and save to memory cache
            val bitmap = decodeBitmapFromFile(file, maxWidth, maxHeight)
            bitmap?.let { 
                memoryCache.put(cacheKey, it)
                it
            }
        }
    }
    
    private suspend fun downloadImage(url: String, file: File) = withContext(Dispatchers.IO) {
        URL(url).openStream().use { input ->
            FileOutputStream(file).use { output ->
                input.copyTo(output)
            }
        }
    }
    
    private fun decodeBitmapFromFile(file: File, maxWidth: Int, maxHeight: Int): Bitmap? {
        val options = BitmapFactory.Options().apply {
            inJustDecodeBounds = true
        }
        BitmapFactory.decodeFile(file.absolutePath, options)
        
        options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight)
        options.inJustDecodeBounds = false
        
        return BitmapFactory.decodeFile(file.absolutePath, options)
    }
    
    private fun calculateInSampleSize(
        options: BitmapFactory.Options,
        reqWidth: Int,
        reqHeight: Int
    ): Int {
        val height = options.outHeight
        val width = options.outWidth
        var inSampleSize = 1
        
        if (height > reqHeight || width > reqWidth) {
            val halfHeight = height / 2
            val halfWidth = width / 2
            
            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }
        return inSampleSize
    }
    
    fun clearCache() {
        memoryCache.clear()
        // File cache is automatically managed
    }
    
    fun getCacheStats(): String {
        return "Memory cache: ${memoryCache.size} items, " +
               "Hit ratio: ${memoryCache.hitRatio}, " +
               "File cache size: ${fileCache.size} files"
    }
}

iOS/Multiplatform Support Example

// commonMain/src/commonMain/kotlin/cache/DataCacheManager.kt
expect class PlatformCache

class DataCacheManager {
    private val cache = Kache<String, String>(maxSize = 1000) {
        strategy = KacheStrategy.LRU
    }
    
    suspend fun getCachedData(key: String): String? {
        return cache.get(key)
    }
    
    suspend fun cacheData(key: String, data: String) {
        cache.put(key, data)
    }
    
    suspend fun getOrFetch(key: String, fetcher: suspend () -> String): String {
        return cache.getOrPut(key) {
            fetcher()
        }
    }
}

// androidMain/src/androidMain/kotlin/cache/PlatformCache.kt
actual class PlatformCache {
    // Android-specific implementation
}

// iosMain/src/iosMain/kotlin/cache/PlatformCache.kt
actual class PlatformCache {
    // iOS-specific implementation
}