How to build Android Image Loading library using Kotlin
Image Loading is one of the essential tasks for Android Development. I am sure you used different libraries like Picasso, Glide ,…etc.However, I am sure that a lot of us did not ask himself how these libraries actually work, so I am here today to share with you how to build your own Android Image Loading from scratch. After finishing this tutorial you will be able to implement an image loading library with the following features:
- Image Downloading
- LRU InMemory(RAM)Caching
- DiskCaching using DiskLruCacheLibrary
- Clear Cache
- Cancel Loading Task
- Cancel All
Prerequisites
To be able to get most out of this tutorial you will need:
- Android Studio 3.2.1 or higher
- Emulator of phone
- Kotlin Basics
Getting Started
- Create a new Android Studio project
To get started with this tutorial please create a new Android Studio Project with Kotlin Support.
- Create Library Module
To create a library you need to add new Android Library module form
File=>New=>New Module…=>
- Give your library a name
Edit The name and module name for your library, for example, I will name it photon
- Create Core Package
Create a new package inside the Photon module and give it the name core. T hen, create a new Kotlin class and give it the name Photon.kt The entry point for our library called Photon. Photon acts as ImageLoading manager. It has a Singelton design pattern with the following functions: DisplayImage, Clear cache, CancelTask and Cancel All
- Create Cache Package
create a new package inside the Photon module and give it the name cache. Then, create a new Kotlin class and give it the name CacheRepository.kt the CacheRepository is responsible for putting, getting an image in the cache and clear all cache. It implements all function in Image Cache interface so we need to create a new interface called ImageCache.kt with the following functions
package com.imageloadinglib.cache
import android.graphics.Bitmap
interface ImageCache {
fun put(url: String, bitmap: Bitmap)
fun get(url: String): Bitmap?
fun clear()
}
The Image Cache interface has three functions to save certain Bitmap and to get it from cache also to clear all saved Bitmaps. This interface will be implemented by three classes CacheRepository.kt, MemoryCache.kt and DiskCache.kt. Now we need to create the new two files for memory and Disk.
Save Images on RAM:
package com.imageloadinglib.cache
import android.graphics.Bitmap
import android.support.v4.util.LruCache
import android.util.Log
class MemoryCache (newMaxSize: Int) :ImageCache {
private val cache : LruCache<String, Bitmap>
init {
var cacheSize : Int
if (newMaxSize > Config.maxMemory) {
cacheSize = Config.defaultCacheSize
Log.d("memory_cache","New value of cache is bigger than
maximum cache available on system")
} else {
cacheSize = newMaxSize
}
cache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return (value.rowBytes)*(value.height)/1024
}
}
}
override fun put(url: String, bitmap: Bitmap) {
cache.put(url,bitmap)
}
override fun get(url: String): Bitmap? {
return cache.get(url)
}
override fun clear() {
cache.evictAll()
}
}
The Memory Cache saves the bitmap in RAM Using Latest Recently Used Algorithm (LRU). LRU is one of the most used algorithms in caching, you can imagine it like queue where you put or insert the most used items in the front and rarely used items at the end of queue and when you query new item from it you put it in the front so that it is faster in accessing and also when you need to the size exceeds you can drop out from the end of your queue if you need more info please check this link. Back to our Memory cache class, you can find it is a simple class with reference to Android LruCache
private val cache : LruCache<String, Bitmap>
then we have the configuration for this LruCache using init{} block
init {
var cacheSize : Int
if (newMaxSize > Config.maxMemory) {
cacheSize = Config.defaultCacheSize
Log.d("memory_cache","New value of cache is bigger than
maximum cache available on system")
} else {
cacheSize = newMaxSize
}
cache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return (value.rowBytes)*(value.height)/1024
}
}
}
You can find the configuration class here Config.kt
package com.imageloadinglib.cache
class Config {
companion object {
val maxMemory = Runtime.getRuntime().maxMemory() /1024
val defaultCacheSize = (maxMemory/4).toInt()
}
}
The configuration class helps you to put default size for your Android LRUCache for instance here I am providing a quarter of JVM Memory size you can change it according to your needs.
It is time now to create the DiskCaching to our library to be able to save images on Disk even if there is no internet we can retrieve them any time easily.
Save Images on Disk:
package com.imageloadinglib.cache
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.jakewharton.disklrucache.DiskLruCache
import java.io.*
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
class DiskCache private constructor(val context: Context) : ImageCache {
private var cache: DiskLruCache =
DiskLruCache.open(context.cacheDir, 1, 1, 10 * 1024 * 1024)
override fun get(url: String): Bitmap? {
val key = md5(url)
val snapshot: DiskLruCache.Snapshot? = cache.get(key)
return if (snapshot != null) {
val inputStream: InputStream = snapshot.getInputStream(0)
val buffIn = BufferedInputStream(inputStream, 8 * 1024)
BitmapFactory.decodeStream(buffIn)
} else {
null
}
}
override fun put(url: String, bitmap: Bitmap) {
val key = md5(url)
var editor: DiskLruCache.Editor? = null
try {
editor = cache.edit(key)
if (editor == null) {
return
}
if (writeBitmapToFile(bitmap, editor)) {
cache.flush()
editor.commit()
} else {
editor.abort()
}
} catch (e: IOException) {
try {
editor?.abort()
} catch (ignored: IOException) {
}
}
}
override fun clear() {
cache.delete()
cache = DiskLruCache.open(context.cacheDir, 1, 1,
10 * 1024 * 1024)
}
private fun writeBitmapToFile(bitmap: Bitmap, editor:
DiskLruCache.Editor): Boolean {
var out: OutputStream? = null
try {
out = BufferedOutputStream(editor.newOutputStream(0), 8 *
1024)
return bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
} finally {
out?.close()
}
}
fun md5(url: String): String? {
try {
// Static getInstance method is called with hashing MD5
val md = MessageDigest.getInstance("MD5")
// digest() method is called to calculate message digest
// of an input digest() return array of byte
val messageDigest = md.digest(url.toByteArray())
// Convert byte array into signum representation
val no = BigInteger(1, messageDigest)
// Convert message digest into hex value
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
return hashtext
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
// For specifying wrong message digest algorithms
}
companion object {
private val INSTANCE: DiskCache? = null
@Synchronized
fun getInstance(context: Context): DiskCache {
return INSTANCE?.let { return INSTANCE }
?: run {
return DiskCache(context)
}
}
}
}
This class depends on DiskLruCache class from **Disk LRU Cache Library**developed by the Jake Wharton. This is a great caching library that implements the LRU Algorithm on Disk Scope. Every entry has key and value the key must match the regex [a-z0-9_-]{1,120}
so we have a method called md5 in our class to handle this as follows:
fun md5(url: String): String? {
try {
// Static getInstance method is called with hashing MD5
val md = MessageDigest.getInstance("MD5")
// digest() method is called to calculate message digest
// of an input digest() return array of byte
val messageDigest = md.digest(url.toByteArray())
// Convert byte array into signum representation
val no = BigInteger(1, messageDigest)
// Convert message digest into hex value
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
return hashtext
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
// For specifying wrong message digest algorithms
}
6.Create async Package
Now we need to download images from the resource(url) and we need to handle multi download so we can use Executor framework to help us to do this we need first to create DownloadTask.kt
package com.imageloadinglib.async
import java.util.concurrent.Callable
abstract class DownloadTask<T> : Callable<T> {
abstract fun download(url: String): T
}
Download Task is an abstract class that extends from Callable which is like Runnable but it returns Future Object because we need to be able to cancel certain Loading Task later so that we used this approach. Also, This Download Task is generic so we can use it to download another file not just photo if we need but with some modification.
Now we need to create DownloadImageTask.kt which extends from Download Task
package com.imageloadinglib.async
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Handler
import android.os.Looper
import android.widget.ImageView
import com.imageloadinglib.cache.CacheRepository
import java.net.HttpURLConnection
import java.net.URL
class DownloadImageTask(
private val url: String,
private val imageView: ImageView,
private val cache: CacheRepository
) : DownloadTask<Bitmap?>() {
override fun download(url: String): Bitmap? {
var bitmap: Bitmap? = null
try {
val url = URL(url)
val conn: HttpURLConnection = url.openConnection() as
HttpURLConnection
bitmap = BitmapFactory.decodeStream(conn.inputStream)
conn.disconnect()
} catch (e: Exception) {
e.printStackTrace()
}
return bitmap
}
private val uiHandler = Handler(Looper.getMainLooper())
override fun call(): Bitmap? {
val bitmap = download(url)
bitmap?.let {
if (imageView.tag == url) {
updateImageView(imageView, it)
}
cache.put(url, it)
}
return bitmap
}
fun updateImageView(imageview: ImageView, bitmap: Bitmap) {
uiHandler.post {
imageview.setImageBitmap(bitmap)
}
}
}
This class will override two methods Call and download and from call method, you just call download method to retrieve the image from the resource in a background thread and after that, you set the bitmap to the image view in UI Thread and also cache the bitmap.
Now we return to our Photon.kt or our image loading manager
package com.imageloadinglib.core
import android.content.Context
import android.graphics.Bitmap
import android.widget.ImageView
import com.imageloadinglib.async.DownloadImageTask
import com.imageloadinglib.async.DownloadTask
import com.imageloadinglib.cache.CacheRepository
import com.imageloadinglib.cache.Config
import java.util.concurrent.Executors
import java.util.concurrent.Future
class Photon private constructor(context: Context, cacheSize: Int) {
private val cache = CacheRepository(context, cacheSize)
private val executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
private val mRunningDownloadList:HashMap<String,Future<Bitmap?>> = hashMapOf()
fun displayImage(url: String, imageview: ImageView, placeholder:
Int) {
var bitmap = cache.get(url)
bitmap?.let {
imageview.setImageBitmap(it)
return
}
?: run {
imageview.tag = url
if (placeholder != null)
imageview.setImageResource(placeholder)
addDownloadImageTask( url, DownloadImageTask(url , imageview , cache)) }
}
fun addDownloadImageTask(url: String,downloadTask: DownloadTask<Bitmap?>) {
mRunningDownloadList.put(url,executorService.submit(downloadTask))
}
fun clearcache() {
cache.clear()
}
fun cancelTask(url: String){
synchronized(this){
mRunningDownloadList.forEach {
if (it.key == url && !it.value.isDone)
it.value.cancel(true)
}
}
}
fun cancelAll() {
synchronized (this) {
mRunningDownloadList.forEach{
if ( !it.value.isDone)
it.value.cancel(true)
}
mRunningDownloadList.clear()
}
}
companion object {
private val INSTANCE: Photon? = null
@Synchronized
fun getInstance(context: Context, cacheSize: Int = Config.defaultCacheSize): Photon {
return INSTANCE?.let { return INSTANCE }
?: run {
return Photon(context, cacheSize)
}
}
}
}
This class will do some specif function like display the image from memory first and if it does not exist it will fetch it from Disk also a clear function which flushes all saved bitmap from memory and Disk. Finally, you can all cancel certain Image loading Task or cancel all tasks.
Sample APP:
Now we need to test our work so that we need to create demo and this is very simple, on your App module create a new activity for example and
paste this code
package com.photon.ui
import android.content.Intent
import com.imageloadinglib.core.Photon
import com.photon.common.CACHE_SIZE
import com.photon.common.URL1
import com.photon.R
import kotlinx.android.synthetic.main.activity_intro.*
class IntroActivity : BaseActivity() {
private lateinit var imageLoader:Photon
val URL1 = "https://i.pinimg.com/originals/93/09/77/930977991c52b48e664c059990dea125.jpg"
override fun initUI() {
imageLoader = Photon.getInstance(this , CACHE_SIZE) //4MiB
imageLoader.displayImage(URL1,image1,R.drawable.place_holder)
listBtn.setOnClickListener {
val intent = Intent(this,MainActivity::class.java)
startActivity(intent)
}
clearBtn.setOnClickListener {
imageLoader.clearcache()
}
}
override fun getLayoutById() = R.layout.activity_intro
}
Conclusion
In this tutorial, we learned a lot of techniques like implement cache repository using Repository Design Pattern, LRU algorithm, Building a new library for Android Development from scratch, Understanding how to create Caching for RAM and Disk in Android to handle Bitmap Caching also how to cancel certain Image Loading Task. Finally, please if you find this tutorial helpful share it with your friends