Home / Android Development / How to build Android Image Loading library using Kotlin

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:

Getting Started

  1. Create a new Android Studio project

    To get started with this tutorial please create a new Android Studio Project with Kotlin Support.

  2. Create Library Module

    To create a library you need to add new Android Library module form
    File=>New=>New Module…=>new library module

  3. Give your library a name

    Edit The name and module name for your library, for example, I will name it photonedit your library name

  4. Create Core Package

    Create a new package inside the Photon module and give it the name core. Then, 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

  5. 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

Check Also

Getting Started to Dagger Multibindings with Android View Model from JetPack

Dagger is a great solution for handling Dependency Injection, Until now we used to provide …

Leave a Reply

Your email address will not be published. Required fields are marked *