Messages and Message Handlers

Your dedicated thread will download photos, but how will it work with the RecyclerView’s adapter to display them when it cannot directly access the main thread? (Recall that background threads are not allowed to execute code that modifies the view – only the main thread is permitted do that. The main thread cannot execute long-running tasks – only a background thread is permitted to do that.)

Think back to the shoe store with two Flashes. Background Flash has wrapped up his phone call to the distributor. He needs to tell Main Flash that the shoes are back in stock. If Main Flash is busy, Background Flash cannot do this right away. He would have to wait by the register to catch Main Flash at a spare moment. This would work, but it would not be very efficient.

The better solution is to give each Flash an inbox. Background Flash writes a message about the shoes being in stock and puts it on top of Main Flash’s inbox. Main Flash does the same thing when he wants to tell Background Flash that the stock of shoes has run out.

The inbox idea turns out to be really handy. The Flash (either Flash, that is) may have something that needs to be done soon, but not right at the moment. In that case, he can put a message in his own inbox and then handle it when he has time.

In Android, the inbox that threads use is called a message queue. A thread that works by using a message queue is called a message loop; it loops again and again looking for new messages on its queue (Figure 25.2).

Figure 25.2  Flash dance

Flash dance

A message loop consists of a thread and a looper. The Looper is the object that manages a thread’s message queue.

The main thread is a message loop and has a looper. Everything your main thread does is performed by its looper, which grabs messages off of its message queue and performs the task they specify (Figure 25.3).

Figure 25.3  Main thread is a HandlerThread

Main thread is a HandlerThread

You are going to create a background thread that is also a message loop. You will use a class called HandlerThread that prepares a Looper for you.

The main thread and your background thread will communicate with each other by placing messages in each other’s queue using Handlers (Figure 25.4).

Figure 25.4  Communicating with Handlers

Communicating with Handlers

Before you create a message, you need to understand what a Message is and the relationship it has with its Handler (often called its message handler).

Message anatomy

Let’s start by looking closely at messages. The messages that a Flash might put in an inbox (its own inbox or that of another Flash) are not supportive notes, like, “You run very fast, Flash.” They are tasks that need to be handled.

A message is an instance of Message and contains several fields. Three are relevant to your implementation:

what

a user-defined Int that describes the message

obj

a user-specified object to be sent with the message

target

the Handler that will handle the message

The target of a Message is an instance of Handler. You can think of the name Handler as being short for message handler. When you create a Message, it will automatically be attached to a Handler. And when your Message is ready to be processed, Handler will be the object in charge of making it happen.

Handler anatomy

To do any real work with messages, you first need an instance of Handler. A Handler is not just a target for processing your Messages. A Handler is your interface for creating and posting Messages, too. Take a look at Figure 25.5.

Figure 25.5  Looper, Handler, HandlerThread, and Messages

Looper, Handler, HandlerThread, and Messages

Messages must be posted and consumed on a Looper, because the Looper owns the inbox of Message objects. So the Handler always has a reference to its coworker, the Looper.

A Handler is attached to exactly one Looper, and a Message is attached to exactly one target Handler, called its target. A Looper has a whole queue of Messages. Multiple Messages can reference the same target Handler (Figure 25.5).

Multiple Handlers can be attached to one Looper (Figure 25.6). This means that your Handler’s Messages may be living side by side with another Handler’s messages.

Figure 25.6  Multiple Handlers, one Looper

Multiple Handlers, one Looper

Using handlers

Usually, you do not set a message’s target Handler by hand. It is better to build the message by calling Handler.obtainMessage(…). You pass the other message fields into this function, and it automatically sets the target to the Handler object the function was called on for you.

Handler.obtainMessage(…) pulls from a common recycling pool to avoid creating new Message objects, so it is also more efficient than creating new instances.

Once you have obtained a Message, you call sendToTarget() to send the Message to its Handler. The Handler will then put the Message on the end of Looper’s message queue.

In this case, you are going to obtain a message and send it to its target within the implementation of queueThumbnail(). The message’s what will be a constant defined as MESSAGE_DOWNLOAD. The message’s obj will be an object of type T, which will be used to identify the download. In this case, obj will be the PhotoHolder that the adapter passed in to queueThumbnail().

When the looper pulls a Message from the queue, it gives the message to the message’s target Handler to handle. Typically, the message is handled in the target’s implementation of Handler.handleMessage(…).

Figure 25.7 shows the object relationships involved.

Figure 25.7  Creating a Message and sending it

Creating a Message and sending it

In this case, your implementation of handleMessage(…) will use FlickrFetchr to download bytes from the URL and then turn those bytes into a bitmap.

First, add the constant and properties shown in Listing 25.13.

Listing 25.13  Adding a constant and properties (ThumbnailDownloader.kt)

private const val TAG = "ThumbnailDownloader"
private const val MESSAGE_DOWNLOAD = 0

class ThumbnailDownloader<in T>
    : HandlerThread(TAG), LifecycleObserver {

    private var hasQuit = false
    private lateinit var requestHandler: Handler
    private val requestMap = ConcurrentHashMap<T, String>()
    private val flickrFetchr = FlickrFetchr()
    ...
}

MESSAGE_DOWNLOAD will be used to identify messages as download requests. (ThumbnailDownloader will set this as the what on any new download messages it creates.)

The newly added requestHandler will store a reference to the Handler responsible for queueing download requests as messages onto the ThumbnailDownloader background thread. This handler will also be in charge of processing download request messages when they are pulled off the queue.

The requestMap property is a ConcurrentHashMap, a thread-safe version of HashMap. Here, using a download request’s identifying object of type T as a key, you can store and retrieve the URL associated with a particular request. (In this case, the identifying object is a PhotoHolder, so the request response can be easily routed back to the UI element where the downloaded image should be placed.)

The flickrFetchr property stores a reference to a FlickrFetchr instance. This way all of the Retrofit setup code will only execute once during the lifetime of the thread. (Creating and configuring a new Retrofit instance every time you make a web request can slow your app down, especially if you are doing many requests in rapid succession.)

Next, add code to queueThumbnail(…) to update requestMap and to post a new message to the background thread’s message queue.

Listing 25.14  Obtaining and sending a message (ThumbnailDownloader.kt)

class ThumbnailDownloader<in T>
    : HandlerThread(TAG), LifecycleObserver {
    ...
    fun queueThumbnail(target: T, url: String) {
        Log.i(TAG, "Got a URL: $url")
        requestMap[target] = url
        requestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
            .sendToTarget()
    }
}

You obtain a message directly from requestHandler, which automatically sets the new Message object’s target field to requestHandler. This means requestHandler will be in charge of processing the message when it is pulled off the message queue. The message’s what field is set to MESSAGE_DOWNLOAD. Its obj field is set to the T target value (a PhotoHolder, in this case) that is passed to queueThumbnail(…).

The new message represents a download request for the specified T target (a PhotoHolder from the RecyclerView). Recall that RecyclerView’s adapter implementation in PhotoGalleryFragment calls queueThumbnail(…) from onBindViewHolder(…), passing along the PhotoHolder the image is being downloaded for and the URL location of the image to download.

Notice that the message itself does not include the URL. Instead, you update requestMap with a mapping between the request identifier (PhotoHolder) and the URL for the request. Later you will pull the URL from requestMap to ensure that you are always downloading the most recently requested URL for a given PhotoHolder instance. (This is important because ViewHolder objects in RecyclerViews are recycled and reused.)

Finally, initialize requestHandler and define what that Handler will do when downloaded messages are pulled off the queue and passed to it.

Listing 25.15  Handling a message (ThumbnailDownloader.kt)

class ThumbnailDownloader<in T>
    : HandlerThread(TAG), LifecycleObserver {
    ...
    private val requestMap = ConcurrentHashMap<T, String>()
    private val flickrFetchr = FlickrFetchr()

    @Suppress("UNCHECKED_CAST")
    @SuppressLint("HandlerLeak")
    override fun onLooperPrepared() {
        requestHandler = object : Handler() {
            override fun handleMessage(msg: Message) {
                if (msg.what == MESSAGE_DOWNLOAD) {
                    val target = msg.obj as T
                    Log.i(TAG, "Got a request for URL: ${requestMap[target]}")
                    handleRequest(target)
                }
            }
        }
    }
    ...
    fun queueThumbnail(target: T, url: String) {
        ...
    }

    private fun handleRequest(target: T) {
        val url = requestMap[target] ?: return
        val bitmap = flickrFetchr.fetchPhoto(url) ?: return
    }
}

When importing Message, make sure to select android.os.Message from the options presented.

You implemented Handler.handleMessage(…) in your Handler subclass within onLooperPrepared(). HandlerThread.onLooperPrepared() is called before the Looper checks the queue for the first time. This makes it a good place to create your Handler implementation.

Within Handler.handleMessage(…), you check the message type, retrieve the obj value (which will be of type T and serves as the identifier for the request), and then pass it to handleRequest(…). (Recall that Handler.handleMessage(…) will get called when a download message is pulled off the queue and ready to be processed.)

The handleRequest() function is a helper function where the downloading happens. Here you check for the existence of a URL. Then you pass the URL to the FlickrFetchr.fetchPhoto(…) function that you created with such foresight at the beginning of this chapter.

The @Suppress("UNCHECKED_CAST") annotation tells the Lint checker that you are well aware that you are casting msg.obj to type T without first checking whether msg.obj is indeed of type T. This is OK for now because you are the only developer dealing with your PhotoGallery code. You control messages added to the queue, and you know that at this point all the messages you queue have their obj field set to a PhotoHolder instance (which matches the T specified on ThumbnailDownloader).

The way you create the Handler implementation above technically creates an inner class. Inner classes hold a reference to their outer class (ThumbnailDownloader in this case), which can in turn cause the outer class to leak if the lifetime of the inner class is longer than the intended lifetime of the outer class.

This is only problematic if your handler is attached to the main thread’s looper. You suppress a HandlerLeak warning using @SuppressLint("HandlerLeak"), since the handler you create is attached to the looper of a background thread. If the handler were instead attached to the main thread’s looper, it might not be garbage-collected. If it were to leak, since it also holds a reference to the ThumbnailDownloader, your app would leak the ThumbnailDownloader instance as well.

In general, you should only suppress Lint warnings if you truly understand the warning and why suppressing it in the given scenario is safe.

Run PhotoGallery and check Logcat for your confirming log statements.

Of course, the request will not be completely handled until you set the bitmap on the PhotoHolder that originally came from PhotoAdapter. However, that is UI work, so it must be done on the main thread.

Everything you have seen so far uses handlers and messages on a single thread – ThumbnailDownloader putting messages in ThumbnailDownloader’s own inbox. In the next section, you will see how ThumbnailDownloader can use a Handler to post requests to a separate thread (namely, the main thread).

Passing handlers

So far you are able to schedule work on the background thread from the main thread using ThumbnailDownloader’s requestHandler. This flow is shown in Figure 25.8.

Figure 25.8  Scheduling work on ThumbnailDownloader from the main thread

Scheduling work on ThumbnailDownloader from the main thread

You can also schedule work on the main thread from the background thread using a Handler attached to the main thread. This flow looks like Figure 25.9.

Figure 25.9  Scheduling work on the main thread from ThumbnailDownloader’s thread

Scheduling work on the main thread from ThumbnailDownloader’s thread

The main thread is a message loop with handlers and a Looper. When you create a Handler in the main thread, it will be associated with the main thread’s Looper. You can then pass that Handler to another thread. The passed Handler maintains its loyalty to the Looper of the thread that created it. Any messages the Handler is responsible for will be handled on the main thread’s queue.

In ThumbnailDownloader.kt, add the responseHandler property seen above to hold a Handler passed from the main thread. Then replace the constructor with one that accepts a Handler (which will be stored in responseHandler) and a function type that will be used as a callback to communicate the responses (downloaded images) with the requester (the main thread).

Listing 25.16  Adding the responseHandler (ThumbnailDownloader.kt)

class ThumbnailDownloader<in T>(
    private val responseHandler: Handler,
    private val onThumbnailDownloaded: (T, Bitmap) -> Unit
) : HandlerThread(TAG), LifecycleObserver {
    ...
}

The function type property defined in your new constructor will eventually be called when an image has been fully downloaded and is ready to be added to the UI. Using this listener delegates the responsibility of what to do with the downloaded image to a class other than ThumbnailDownloader (in this case, to PhotoGalleryFragment). Doing so separates the downloading task from the UI updating task (putting the images into ImageViews), so that ThumbnailDownloader could be used for downloading into other kinds of View objects as needed.

Next, modify PhotoGalleryFragment to pass a Handler attached to the main thread to ThumbnailDownloader. Also, pass an anonymous function to handle the downloaded image once it is complete.

Listing 25.17  Hooking up to response Handler (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        thumbnailDownloader = ThumbnailDownloader()
        val responseHandler = Handler()
        thumbnailDownloader =
                ThumbnailDownloader(responseHandler) { photoHolder, bitmap ->
                    val drawable = BitmapDrawable(resources, bitmap)
                    photoHolder.bindDrawable(drawable)
                }
        lifecycle.addObserver(thumbnailDownloader)
    }
    ...
}

Remember that by default, the Handler will attach itself to the Looper for the current thread. Because this Handler is created in onCreate(…), it will be attached to the main thread’s Looper.

Now ThumbnailDownloader has access via responseHandler to a Handler that is tied to the main thread’s Looper. It also has your function type implementation to do the UI work with the returning Bitmaps. Specifically, the function passed to the onThumbnailDownloaded higher-order function sets the Drawable of the originally requested PhotoHolder to the newly downloaded Bitmap.

You could send a custom Message back to the main thread requesting to add the image to the UI, similar to how you queued a request on the background thread to download the image. However, this would require another subclass of Handler, with an override of handleMessage(…).

Instead, use another handy Handler function – post(Runnable).

Handler.post(Runnable) is a convenience function for posting Messages that look like this:

    var myRunnable: Runnable = object : Runnable {
        override fun run() {
            // Your code here
        }
    }
    var msg: Message = Message.obtain(someHandler, myRunnable)
    // Sets msg.callback to myRunnable

When a Message has its callback field set, it is not routed to its target Handler when pulled off the message queue. Instead, the run() function of the Runnable stored in callback is executed directly.

In ThumbnailDownloader.handleRequest(), post a Runnable to the main thread’s queue through responseHandler (Listing 25.18).

Listing 25.18  Downloading and displaying images (ThumbnailDownloader.kt)

class ThumbnailDownloader<in T>(
    private val responseHandler: Handler,
    private val onThumbnailDownloaded: (T, Bitmap) -> Unit
) : HandlerThread(TAG), LifecycleObserver {
    ...
    private fun handleRequest(target: T) {
        val url = requestMap[target] ?: return
        val bitmap = flickrFetchr.fetchPhoto(url) ?: return

        responseHandler.post(Runnable {
            if (requestMap[target] != url || hasQuit) {
                return@Runnable
            }

            requestMap.remove(target)
            onThumbnailDownloaded(target, bitmap)
        })
    }
}

Because responseHandler is associated with the main thread’s Looper, all of the code inside of Runnable’s run() will be executed on the main thread.

So what does this code do? First, you double-check the requestMap. This is necessary because the RecyclerView recycles its views. By the time ThumbnailDownloader finishes downloading the Bitmap, RecyclerView may have recycled the PhotoHolder and requested a different URL for it. This check ensures that each PhotoHolder gets the correct image, even if another request has been made in the meantime.

Next, you check hasQuit. If ThumbnailDownloader has already quit, it may be unsafe to run any callbacks.

Finally, you remove the PhotoHolder-URL mapping from the requestMap and set the bitmap on the target PhotoHolder.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.221.67