Assembling a Background Thread

Create a new class called ThumbnailDownloader that extends HandlerThread. Then give it a constructor, a stub implementation of a function called queueThumbnail(), and an override of the quit() function that signals when your thread has quit. (Toward the end of the chapter, you will need this bit of information.)

Listing 25.7  Initial thread code (ThumbnailDownloader.kt)

private const val TAG = "ThumbnailDownloader"

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

    private var hasQuit = false

    override fun quit(): Boolean {
        hasQuit = true
        return super.quit()
    }

    fun queueThumbnail(target: T, url: String) {
        Log.i(TAG, "Got a URL: $url")
    }
}

Notice that you gave the class a single generic argument, <T>. Your ThumbnailDownloader’s user, PhotoGalleryFragment in this case, will need to use some object to identify each download and to determine which UI element to update with the image once it is downloaded. Rather than locking the user into a specific type of object as the identifier, using a generic makes the implementation more flexible.

The queueThumbnail() function expects an object of type T to use as the identifier for the download and a String containing the URL to download. This is the function you will have PhotoAdapter call in its onBindViewHolder(…) implementation.

Making your thread lifecycle aware

Since the sole purpose of ThumbnailDownloader is to download and serve images to PhotoGalleryFragment, tie the life of the thread to the user’s perceived lifetime of the fragment. In other words, spin the thread up when the user first launches the screen. Spin the thread down when the user finishes with the screen (e.g., by pressing the Back button or dismissing the task). Do not destroy and then re-create the thread when the user rotates the device – retain the thread instance across configuration changes.

The life of a ViewModel matches the user’s perceived life of the fragment. However, managing the thread in PhotoGalleryViewModel would make the implementation more complex than necessary and also cause some challenges with leaking views down the line. It would make more sense to push the thread management to some other component, such as the repository (FlickrFetchr). In a real-world app, you would likely do just that. But in this case, doing so would detract from the goal of understanding HandlerThread.

Instead, for the purposes of this chapter, directly tie a ThumbnailDownloader instance to your PhotoGalleryFragment. First, retain PhotoGalleryFragment so that the life of the fragment instance matches the user’s perceived life of the fragment.

Listing 25.8  Retaining PhotoGalleryFragment (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        retainInstance = true
        ...
    }
    ...
}

(In general, you should avoid retaining fragments. You are only doing it here because retaining the fragment simplifies the implementation so you can focus on learning how HandlerThread works. You will learn more about the implications of retaining a fragment in the section called Retained Fragments later in this chapter.)

Now that the fragment is retained, start the thread when PhotoGalleryFragment.onCreate(…) is called and quit the thread when PhotoGalleryFragment.onDestroy() is called. You could achieve this by adding code to PhotoGalleryFragment’s lifecycle functions directly, but this would add unnecessary complexity to your fragment class. Instead, abstract the code into ThumbnailDownloader by making ThumbnailDownloader lifecycle aware.

A lifecycle-aware component, known as a lifecycle observer, observes the lifecycle of a lifecycle owner. Activity and Fragment are examples of lifecycle owners – they have a lifecycle and implement the LifecycleOwner interface.

Update ThumbnailDownloader to implement the LifecycleObserver interface and observe the onCreate(…) and onDestroy functions of its lifecycle owner. Have ThumbnailDownloader start itself when onCreate(…) is called and stop itself when onDestroy() is called.

Listing 25.9  Making ThumbnailDownloader lifecycle aware (ThumbnailDownloader.kt)

private const val TAG = "ThumbnailDownloader"

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

    private var hasQuit = false

    override fun quit(): Boolean {
        hasQuit = true
        return super.quit()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun setup() {
        Log.i(TAG, "Starting background thread")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun tearDown() {
        Log.i(TAG, "Destroying background thread")
    }

    fun queueThumbnail(target: T, url: String) {
        Log.i(TAG, "Got a URL: $url")
    }
}

Implementing LifecycleObserver means you can register ThumbnailDownloader to receive lifecycle callbacks from any LifecycleOwner. You use the @OnLifecycleEvent(Lifecycle.Event) annotation to associate a function in your class with a lifecycle callback. Lifecycle.Event.ON_CREATE registers ThumbnailDownloader.setup() to be called when LifecycleOwner.onCreate(…) is called. Lifecycle.Event.ON_DESTROY registers ThumbnailDownloader.tearDown() to be called when LifecycleOwner.onDestroy() is called.

You can view a list of available Lifecycle.Event constants by visiting the API reference page (developer.android.com/​reference/​android/​arch/​lifecycle/​Lifecycle.Event).

(By the way, LifecycleObserver, Lifecycle.Event, and OnLifecycleEvent are part of Jetpack, in the android.arch.lifecycle package. You already have access to these classes because you added the lifecycle-extensions dependency to your Gradle file in Chapter 24.)

Next, you need to create an instance of ThumbnailDownloader and register it to receive lifecycle callbacks from PhotoGalleryFragment. Do that in PhotoGalleryFragment.kt.

Listing 25.10  Creating a ThumbnailDownloader (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoGalleryViewModel: PhotoGalleryViewModel
    private lateinit var photoRecyclerView: RecyclerView
    private lateinit var thumbnailDownloader: ThumbnailDownloader<PhotoHolder>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        retainInstance = true

        photoGalleryViewModel =
                ViewModelProviders.of(this).get(PhotoGalleryViewModel::class.java)

        thumbnailDownloader = ThumbnailDownloader()
        lifecycle.addObserver(thumbnailDownloader)
    }
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
    }

    override fun onDestroy() {
        super.onDestroy()
        lifecycle.removeObserver(
            thumbnailDownloader
        )
    }
    ...
}

You can specify any type for ThumbnailDownloader’s generic argument. However, recall that this argument specifies the type of the object that will be used as the identifier for your download. In this case, the PhotoHolder makes for a convenient identifier, since it is also the target where the downloaded images will eventually go.

Since Fragment implements LifecycleOwner, it has a lifecycle property. You use this property to add an observer to the fragment’s Lifecycle. Calling lifecycle.addObserver(thumbnailDownloader) registers the thumbnail downloader instance to receive the fragment’s lifecycle callbacks. Now, when PhotoGalleryFragment.onCreate(…) is called, ThumbnailDownloader.setup() gets called. When PhotoGalleryFragment.onDestroy() is called, ThumbnailDownloader.tearDown() gets called.

You call lifecycle.removeObserver(thumbnailDownloader) in Fragment.onDestroy() to remove thumbnailDownloader as a lifecycle observer when the fragment instance is destroyed. You could rely on garbage collection to get rid of the fragment’s lifecycle and lifecycle observers when the garbage collector frees the fragment’s (and possibly activity’s) object graph. However, we prefer deterministic resource deallocation over waiting for the next time scavenging happens. This helps keep bugs close to the event that triggered them.

Run your app. You should still see a wall of close-up Bills. Check your log statements for the message ThumbnailDownloader: Starting background thread to verify that ThumbnailDownloader.setup() got executed one time. Press the Back button to exit the application and finish (and destroy) PhotoGalleryActivity (and the PhotoGalleryFragment it hosts). Check your log statements again; the message ThumbnailDownloader: Destroying background thread verifies that ThumbnailDownloader.tearDown() got executed one time.

Starting and stopping a HandlerThread

Now that ThumbnailDownloader is observing PhotoGalleryFragment’s lifecycle, update ThumbnailDownloader to start itself when PhotoGalleryFragment.onCreate(…) is called and stop itself when PhotoGalleryFragment.onDestroy() is called.

Listing 25.11  Starting and stopping the ThumbnailDownloader thread (ThumbnailDownloader.kt)

class ThumbnailDownloader<in T>
    : HandlerThread(TAG), LifecycleObserver {
    ...
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun setup() {
        Log.i(TAG, "Starting background thread")
        start()
        looper
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun tearDown() {
        Log.i(TAG, "Destroying background thread")
        quit()
    }

    fun queueThumbnail(target: T, url: String) {
        Log.i(TAG, "Got a URL: $url")
    }
}

A couple of safety notes. One: Notice that you access looper after calling start() on your ThumbnailDownloader (you will learn more about the Looper in a moment). This is a way to ensure that the thread’s guts are ready before proceeding, to obviate a potential (though rarely occurring) race condition. Until you first access looper, there is no guarantee that onLooperPrepared() has been called, so there is a possibility that calls to queueThumbnail(…) will fail due to a null Handler.

Safety note number two: You call quit() to terminate the thread. This is critical. If you do not quit your HandlerThreads, they will never die. Like zombies. Or rock and roll.

Finally, within PhotoAdapter.onBindViewHolder(…), call the thread’s queueThumbnail() function and pass in the target PhotoHolder where the image will ultimately be placed and the GalleryItem’s URL to download from.

Listing 25.12  Hooking up ThumbnailDownloader (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    private inner class PhotoAdapter(private val galleryItems: List<GalleryItem>)
        : RecyclerView.Adapter<PhotoHolder>() {
        ...
        override fun onBindViewHolder(holder: PhotoHolder, position: Int) {
            val galleryItem = galleryItems[position]
            ...
            thumbnailDownloader.queueThumbnail(holder, galleryItem.url)
        }
    }
}

Run PhotoGallery again and check out Logcat. When you scroll around the RecyclerView, you should see lines in Logcat signaling that ThumbnailDownloader is getting each one of your download requests. You will still see a wall of Bills in the app (do not fear, you will fix this soon enough).

Now that you have a HandlerThread up and running, the next step is to communicate between your app’s main thread and your new background thread.

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

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