Advertisements

How to download images inside RecyclerView

In this article we will find out how to download images inside RecyclerView using only an HandlerThread. This is a follow up on my previous post about Handler and Looper. That post was mostly theoretical, this time we will look at a real implementation of something that can be useful in real life. How to download and display images inside a RecyclerView without using libraries.

There are tons of libraries out there that we can use, and the suggestion is of course, use them. We can save a lot of time, really. Do not reinvent the wheel.
However. There are several reasons why we might want or need to avoid libraries.

  • We just received a coding challenge that requires it.
  • The company’s policy might be against having too many dependencies in the code.
  • We like to have full control over the code, and the ability to tweak it in its minute details.
  • Maintainers of a library can discontinue support anytime forcing you to rewrite parts of the code.
  • The cost of using libraries all the time is that we do not learn how things really work.
  • We feel useless thinking that programming nowadays is just putting together the right libraries. I’m with you on this one.

 

RecyclerView showing images downloaded with an HandlerThread
See the complete code on GitHub

 

To be honest I did use two libraries for the demo app. One library is Google Gson to parse a small json file, parsing is extremely boring and I am thankful we have libraries for it. The other library is the RecyclerView support library.

Introduction

I felt like adding some extra background for those who just started their journey into Android development, if you know this already you can skip to the next section.
Downloading images is a blocking operation, meaning that when we need to download an image we have to stop what we are doing and wait for it to be available. The image could be huge, or the network could be very slow, or the server could be behaving funny. The point is that it is a risky operation to block the application’s main thread for an operation that depends on something as unreliable as the network.

It gets worse, in an attempt to prevent developers from blocking for too long, Android introduced a safety mechanism, the ANR or Application Not Responding error. The system triggers an ANR whenever it detects that an App became unresponsive. That’s why we sometimes get error dialogs asking to wait some more or to quit the application.

How does the system know when to trigger the ANR? It monitors the MessageQueue of the main thread, also called UI thread. A thread is a flow of execution, and many threads can exist and run independently at the same time. The UI thread is the flow of execution responsible for updating the UI. It does its job by handling messages in a MessageQueue. A user’s click on a Button puts a new message on the MessageQueue. The MessageQueue contains one message for every event generated by the UI. The UI thread uses a Looper to continuously handle messages that are put on the MessageQueue, and eventually the onClickListener is invoked.

It is important to know that the code inside onClickListener class is still executed by the UI thread. As soon as onClickListener returns the UI thread goes back to the MessageQueue and processes the other events. If we start downloading an image in there, or doing some other blocking operation, the main thread won’t be able to handle the other messages in the MessageQueue. This will blocking the entire application, triggering the ANR. This is why someone else has to take care of blocking operations.

Handlers and Loopers

The approach relies heavily on Handler and Looper, if you are not familiar with them I suggest you have a look at my post about Handler and Looper. It will give you the theoretical foundation to understand what follows.

The HandlerThread

At the end of the post about Handler and Looper we mentioned the HandlerThread as an easy way to create a looper thread. HandlerThread is a type of Thread that automatically prepares and starts a looper, so we can use it out of the box to create an handler and send and receive messages to it. We do not have to worry about waiting until the looper is ready, as we did previously. When we start an HandlerThread we can simply call getLooper() on it to obtain the looper object, the call will block and wait until the looper is ready. It is still a blocking operation, as it is downloading images, however it is much more predictable as it does not depend on the network but just on the HandlerThread starting. Therefore it is safe.

How it works

We need two Handlers, bgHandler and uiHandler.
The bgHandler is associated to the HandlerThread.
The uiHandler is associated to the UI Thread.
When the RecyclerView’s Adapter needs an image it sends a message to bgHandler with the instruction to download an image. The download starts and the HandlerThread’s execution safely blocks until the image is available. When the image is finally available bgHandler sends a message to uiHandler with the instruction to display that image. HandlerThread then moves on to the next image and the cycle repeats. Remember that in Android it is illegal to access UI widgets from a thread that is not the main thread. This is why we need uiHandler.

The Demo App

The structure of the demo app is very simple, informations about the images is stored in a json file from the assets/ folder. It contains an array of entries like this

{
  "name": "Amazing waterfall",
  "author": "James A. Scott",
  "url": "https://thecodebutchery.com/wp-content/uploads/2018/10/waterfall.jpeg"
}

We parse the json file into an array, which we then use in a RecyclerView.Adapter subclass.

The Adapter

Let’s now have a look at the adapter


package com.codebutchery.recyclerviewimagesdownloader

import ...

class ImagesAdapter(private val context: Context) : RecyclerView.Adapter() {
    private val downloaderThread: HandlerThread = HandlerThread("downloaderThread")
    private val bgHandler: Handler
    private val uiHandler: ImageShowHandler = ImageShowHandler(Looper.getMainLooper())
    private val imageDescriptors:Array

Just a subclass of RecyclerView.Adapter. It holds references to the HandlerThread used for downloading the images, and to the Handler that is associated with it. Note that the downloaderThread is created when ImagesAdapter is created, however it is not started yet at this time.
An instance of ImageShowHandler named uiHandler is also created here and associated with the UI Thread using Looper.getMainLooper().
The adapter also contains an Array of ImageDescriptor objects. ImageDescriptor is a Kotlin data class that I use to store the informations for each row as they come from the json file, namely the title of the image, the author’s name and the url.


    init {
        downloaderThread.start()
        bgHandler = ImageDownloadHandler(downloaderThread.looper)

Inside the init block we start the downloaderThread and create the bgHandler using the looper object from downloaderThread.
Next thing to do if parse the json file and prepare the array imageDescriptors.


        val jsonContent = context.assets.open("images.json")
                .bufferedReader()
                .use { it.readText() }

        imageDescriptors = Gson().fromJson(jsonContent , Array::class.java)
    }

Let’s now move on the the RecyclerView.Adapter methods. We have to implement them.


    override fun getItemCount(): Int {
        return imageDescriptors.size
    }

This one is easy, it tells the RecyclerView how many rows to show.


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagesViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.image_entry_view, parent, false)
        return ImagesViewHolder(view)
    }

Also easy, create the ViewHolder by inflating R.layout.image_entry_view and return it.

Binding the views

The onBindViewHolder() method is where some of the interesting stuff happens, this method receives a ViewHolder that was created by onCreateViewHolder() (or recycled by the RecyclerView) and sets up the view for a specific position.


    override fun onBindViewHolder(holder: ImagesViewHolder, position: Int) {
        holder.view.setBackgroundColor(
                if (position % 2 == 0) context.resources.getColor(R.color.evenListItem)
                else context.resources.getColor(R.color.oddListItem))

The first three lines in this method are just UI sugar to display the rows in alternate colours.
Next we setup all the text, image name and image author, we can of course do this immediately.


        holder.tvName.text = imageDescriptors[position].name
        holder.tvAuthor.text = "By " + imageDescriptors[position].author
        holder.ivImage.setImageResource(R.drawable.ic_picture)

We want the ImageView in every row to show a placeholder while we wait for the corresponding image.
Setting a placeholder here is important to avoid flickering when scrolling the RecyclerView. Without a placeholder, a recycled view will still contain the old picture, until it can display the new one.


        val message = Message()
        message.obj = holder.ivImage
        message.what = 1
        message.data = Bundle()
        message.data.putString("imageUrl", imageDescriptors[position].url)

        bgHandler.sendMessage(message)
    }

At this point it is time to tell bgHandler that we want the image, therefore we build a Message instance and put some relevant data into it. The message’s obj contains a reference to the ImageView which is the destination for the image, this will be useful later on. The what = 1 is a pure formality, but since bgHandler can receive many types of messages we have to assign an identifier to all of them to distinguish them. However the only message we are sending to bgHandler is the “please download this” message so the what is pretty useless in this case. Finally we have to send the url along with the message, so bgHandler knows what to download. It is easily done by adding a Bundle dictionary to the message.

Dealing with recycled views

Let’s now move on to another, very important, method.


    override fun onViewRecycled(holder: ImagesViewHolder) {
        bgHandler.removeMessages(1, holder.ivImage)
        uiHandler.removeMessages(1, holder.ivImage)
    }

When a view is recycled by the RecyclerView we have to cancel the pending messages related to that view. Why? Imagine that the RecyclerView gets scrolled really fast, download requests will start to be served by the HandlerThread one at a time. If a view is recycled before the HandlerThread has the chance to process the corresponding message we end up with two download requests for the same view inside the MessageQueue, this results in flickering images as the ImageView is updated after every request completes. Using removeMessages() removes all pending messages that have obj set to the ImageView that is being recycled.

Stopping the HandlerThread

Another important method is onDetachedFromRecyclerView(), RecyclerView calls this method as soon as we remove the association with the adapter. The demo app does this inside the activity’s onDestroy() method.


    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        downloaderThread.quit()
    }

Without a call to quit() that quits the looper, the downloaderThread would run forever, causing a memory leak.

The ImageDownloadHandler

Let’s now have a look at ImageDownloadHandler


class ImageDownloadHandler(looper:Looper, private val uiHandler:ImageShowHandler) : Handler(looper) {

We create a new Handler subclass, the constructor accepts the looper object and a reference to the uiHandler. It is important to have this reference here. We will send the “display image” message to it as soon as we have the image.

The ImageDownloadHandler handles only one type of message by downloading the image associated with it. In the adapter we set the url of the image inside the data Bundle of the message. In the ImageDownloadHandler we retrieve the url and use it as an argument to downloadImage().
We will skip the downloadImage() implementation here, it is not that relevant. The only thing to keep in mind is that it can block the execution for a while before it returns. If you want to have a look at how it is implemented use this link.


    override fun handleMessage(inMessage: Message) {
        val url = inMessage.data["imageUrl"] as String

        downloadImage(url)?.let {
            val ivImage = inMessage.obj as ImageView
            val outMessage = Message()
            outMessage.obj = ivImage
            outMessage.what = 1
            outMessage.data = Bundle()
            outMessage.data.putByteArray("imageData", bitmapToByteArray(it))

            val newMessagesEnqueuedForThisImageView = hasMessages(1, ivImage)
            if (!newMessagesEnqueuedForThisImageView) {
                uiHandler.sendMessage(outMessage)
            }
        }
    }

The let block is executed only after downloadImage() returns, inside let we can use it as a reference to the downloaded image as a Bitmap object.

Inside the let block we have to create another Message and send it to uiHandler for execution on the UI thread. This message has an associated obj which is again the ImageView where this image will be displayed. There is one more method that we will skip here, bitmapToByteArray(). It converts the Bitmap object into a ByteArray object that we can put inside the outMessage.data bundle. The uiHandler will then be able to retrieve the image data and display it.
Notice the if block that surrounds the call to sendMessage(), why is that needed?


val newMessagesEnqueuedForThisImageView = hasMessages(1, ivImage)
if (!newMessagesEnqueuedForThisImageView) {
    uiHandler.sendMessage(outMessage)
}

We know that the downloadImage() method can take a while to return, therefore the let block can also take a while. This could easily be 2-3 seconds on a slow network. What happens if the user keeps scrolling and ivImage is recycled while we are downloading the image to put inside it? Without the if check we finish downloading the image and we send the message to uiHandler to update ivImage. However there is already another message in the MessageQueue that will end up updating ivImage again. We end up with images flickering and changing inside the RecyclerView before finally displaying the correct image. The solution is to use hasMessages(). It checks if any new message related to this ivImage was added to the MessageQueue while we were downloading. If that is the case we have to avoid sending the message to the ImageShowHandler.

The ImageShowHandler

The last piece of the puzzle is the handler associated with the UI thread that receives both the bitmap as a ByteArray and the ImageView where to show it.


class ImageShowHandler(looper:Looper) : Handler(looper) {
    override fun handleMessage(inMessage: Message) {
        val ivImage = inMessage.obj as ImageView
        val imageByteArray = inMessage.data["imageData"] as ByteArray
        val bitmap = BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.size)
        ivImage.setImageBitmap(bitmap)
    }
}

This Handler just like ImageDownloadHandler handles only one type of message, this time it is the “Hey, show this image inside this view please” message coming from ImageDownloadHandler. There is nothing really complex going on here. The Handler retrieves the image data as a ByteArray and converts it into a Bitmap object. The obj field of the message contains the associated ImageView therefore it is easy to display the image in it.

Conclusion

And that is how to download images inside RecyclerView. This code is a very basic implementation, there are many possible improvements and probably I will write more posts about this. The most obvious improvement is to use a cache instead of converting the Bitmap to a ByteArray and passing it along inside the Message. This approach avoids downloading the images multiple times and makes everything faster. Another issue with this code is that the downloading order of the images inside the RecyclerView depends on the scrolling direction, try it. Not cool, can you come up with a solution?

I hope this was useful, I also hope it was clear enough. I suggest once more that you have a look at the demo app on GitHub, and please feel free to comment to suggest improvements or idea for new tutorials.

Unique opportunity! Help a fellow grow his blog!

Hi there! If you’ve read this far maybe you think this was useful, or fun, or I don’t know what but for some reason You Got Here! Great! Please consider sharing this post with your network, I am trying to get The Code Butchery to grow so I can provide more content like this, will you help me in my journey? Thank you!

Advertisements
Share this

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.