Skip to content

Threading

Malachi de AElfweald edited this page Dec 10, 2018 · 39 revisions

Goal

Do some processing on the background thread then update the UI thread.

Table of Contents

Main Thread

You might recall that our onActivityResult is being called on the main (UI) thread.

Let's discuss for a moment what that means.

Each thread in the system is given a name. We saw some of these when we reviewed Debugging.

main is the name given to the primary thread responsible for the UI. There is a queue running on it, and it just keeps executing everything in that queue in order. When you ask for some activity to be opened, an image to be drawn, or touch the screen - each of these things add new events to that queue to be processed. The more items in that queue, the slower and less responsive your application (and in fact the phone) feels, because it takes longer and longer to finally get to that draw call you requested or to respond to that touch event.

ANR

An ANR (or Application Not Responding) happens when the delay is so great that your application does not respond to a request in a timely fashion (generally about 5 seconds).

Let's introduce an arbitrary delay so that you can see how this happens.

Open BookListActivity.

We'll add the code change inside saveJson to mimic a long write operation.

Inside the try block, we will introduce a 20-second delay (it's taking a VERY long time to write!).

private fun saveJson(readingList: ReadingList) {
    try{
        val writer = FileWriter(File(filesDir, JSON_FILE))
        app.gson.toJson(readingList, writer)
        writer.flush()
        writer.close()

        TimeUnit.SECONDS.sleep(20L)
    }catch(e: Exception){
        Log.d(ReadingListApp.TAG, "Failed to save Json to files/$JSON_FILE",e)
    }
}

Use the java.util import, not the icu import.

Note that this delay is inside a try/catch block.

Deploy your application. You will see that you will see a mostly-blank screen for a long time, then it will eventually load.

Why didn't it crash?

Remember that the ANR is caused by failure to respond in time. We need an additional event to be requested.

Redeploy your application, but this time click the back button while the screen is mostly blank. You will notice that the button stays highlighted before eventually the application throws up the ANR message.

ANR

At this stage, it is up to the user whether to wait or close your application. Some users will wait, in which case your application will continue. Unfortunately, since applications that have this kind of problem tend to have it repeatedly, I think most people will just click close and some will even give you a negative review.

If they do close the application, Google will report the crash in your dashboard and it is up to you to figure out why it is happening.

Go ahead and remove the new line of code.

Background Thread

Since the "main" thread is our foreground UI thread, you can think of all other threads as background threads. They are useful if you want to do some long running process without causing an ANR or slowing down the UI.

So why wouldn't we just put everything in a background thread?

CalledFromWrongThreadException

Inside the end of your onCreate, add this snippet

thread {
    Log.d(ReadingListApp.TAG, "On thread: ${Thread.currentThread().name}")
    TimeUnit.SECONDS.sleep(2L)
    viewAdapter.readingList = ReadingList(
        title ="Empty Book List"
    )
}

The intention here is to start a new thread, log which thread it is, wait 2 seconds then update the adapter.

Open your Logcat tab and clear your filtering.

Deploy your application. The app should crash.

You'll have to scroll through your (unfortunately verbose) logs, but you should see

2018-11-26 08:36:51.461 8666-8666/com.aboutobjects.curriculum.readinglist I/ReadingListApp: 18 books loaded
2018-11-26 08:36:51.478 8666-8684/com.aboutobjects.curriculum.readinglist D/ReadingListApp: On thread: Thread-4

followed by

    --------- beginning of crash
2018-11-26 08:36:53.479 8666-8684/com.aboutobjects.curriculum.readinglist E/AndroidRuntime: FATAL EXCEPTION: Thread-4
    Process: com.aboutobjects.curriculum.readinglist, PID: 8666
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at android.view.View.requestLayout(View.java:23093)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3249)
        at android.view.View.requestLayout(View.java:23093)
        at androidx.recyclerview.widget.RecyclerView.requestLayout(RecyclerView.java:4202)
        at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:5286)
        at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11997)
        at androidx.recyclerview.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:7070)
        at com.aboutobjects.curriculum.readinglist.ui.ReadingListAdapter.setReadingList(ReadingListAdapter.kt:22)
        at com.aboutobjects.curriculum.readinglist.BookListActivity$onCreate$4.invoke(BookListActivity.kt:112)
        at com.aboutobjects.curriculum.readinglist.BookListActivity$onCreate$4.invoke(BookListActivity.kt:27)
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

This error tells us CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

So, we can't just always use the background threads either. While we usually want to do any processing in a background thread, we often have to update the UI from the main thread.

Before we remove that temporary block...

Handler

One possible way of throwing items onto the UI queue is using the Handler. While it can be used for background threads, it's generally used for the UI thread.

Define a new variable

private val handler = Handler(Looper.getMainLooper())

Make sure to use the android.os import not the java.util one.

The Looper.getMainLooper parameter is what tells the Handler to use the main thread.

Then, update our thread block

thread {
    Log.d(ReadingListApp.TAG, "On thread: ${Thread.currentThread().name}")
    TimeUnit.SECONDS.sleep(2L)
    handler.post {
        Log.d(ReadingListApp.TAG, "Now on thread: ${Thread.currentThread().name}")
        viewAdapter.readingList = ReadingList(
            title ="Empty Book List"
        )
    }
}

Here we are just posting at the end of the queue. There are other methods for more fine grained control.

Set your Logcat filter back to ReadingListApp and redeploy.

You'll see that your application loads the original list of books, then after 2 seconds, switches to the empty list of books.

In logcat, you'll see

2018-11-26 08:47:44.883 9442-9442/com.aboutobjects.curriculum.readinglist I/ReadingListApp: 18 books loaded
2018-11-26 08:47:44.905 9442-9460/com.aboutobjects.curriculum.readinglist D/ReadingListApp: On thread: Thread-4
2018-11-26 08:47:46.919 9442-9442/com.aboutobjects.curriculum.readinglist D/ReadingListApp: Now on thread: main

Seems simple enough. Let's look at a couple of different ways to handle this.

Remove the thread block as well as the handler variable.

Don't forget to Optimize Imports whenever you remove blocks of code.

AsyncTask

Another possible method that used to be popular was the AsyncTask. While we will not walk through an example of using it, we will briefly discuss it in case you run into it while examining legacy code.

You would extend AsyncTask and provide a code block that would execute first on the UI thread, then another code block on a background thread, then another code block on the UI thread.

The common use case was downloading data from the internet in the background thread, then updating the UI on the UI thread.

Unfortunately, common patterns of usage led to unnecessary calls on the UI thread (remember what happens when it gets clogged up?) as well as calls being made on objects that no longer existed (because the network calls took so long to execute).

If you run into it in legacy code, I would highly recommend tagging it for refactoring (// @TODO). If you would like more information on AsyncTask you can review the JavaDocs.

Anko

Since JetBrains wrote Kotlin and they also wrote Anko, you may be tempted to give it a try. After all it looks like a trivial implementation of what AsyncTask was supposed to do.

Unfortunately, as of this writing, Issue #650 shows that it is not yet compatible with AndroidX and assumes the legacy Android Support library packages.

Rx

The ReactiveX website says:

ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming

There are multiple Rx libraries available to us.

  • RxJava - primary library used to implement ReactiveX on Java-based platforms
  • RxKotlin - additional Kotlin extension functions
  • RxAndroid - Android specific extensions, like the mainThread
  • RxBinding - Android UI specific extensions

build.gradle

To get started, let's update our module-level build.gradle.

implementation "io.reactivex.rxjava2:rxjava:2.2.4"
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

We won't be using the RxBinding at this time.

Sync your project.

BookListActivity

One of the biggest advantages of using Rx is the ability to chain events. Let's try that out by making our load functions happen on a background thread. Of course, we'll have to switch back to the UI thread before we update the UI.

Open BookListActivity.

We're going to be modifying multiple methods here, so let's walk through them one at a time.

loadJsonFromAssets

Let's start with loadJsonFromAssets. Currently, this is called from the UI thread, which means we are reading this XML file on the UI thread. That's unfortunate. Let's fix that.

First, we we will change our return type. Instead of returning a raw ReadingList? we want to return a Maybe<ReadingList>. The Maybe rx class signifies that it might return 0 elements, 1 element or an error.

Once we do that, we need to update our implementation to return a Maybe version.

private fun loadJsonFromAssets(): Maybe<ReadingList> {
    return try{
        val reader = InputStreamReader(assets.open(JSON_FILE))
        Maybe.just(app.gson.fromJson(reader, ReadingList::class.java))
    } catch (e: Exception) {
        Log.d(ReadingListApp.TAG, "Failed to load Json from assets/$JSON_FILE",e)
        Maybe.empty<ReadingList>()
    }
}

You wouldn't normally swallow errors, but we explicitly do not want an error here killing the application. If we wanted to throw an error instead, we could use Maybe.error.

How is this version different than the previous version? This version will allow us to control the threading. We will see that shortly.

loadJsonFromFiles

Do the same thing to loadJsonFromFiles.

private fun loadJsonFromFiles(): Maybe<ReadingList> {
    return try {
        val reader = FileReader(File(filesDir, JSON_FILE))
        Maybe.just(app.gson.fromJson(reader, ReadingList::class.java))
    } catch (e: Exception) {
        Log.d(ReadingListApp.TAG, "Failed to load Json from files/$JSON_FILE",e)
        Maybe.empty<ReadingList>()
    }
}

loadJson

For loadJson, we again want to return a Maybe<ReadingList> but this time we will rely on the Maybe.switchIfEmpty keyword to complete the logic.

private fun loadJson(): Maybe<ReadingList> {
    return loadJsonFromFiles()
        .switchIfEmpty(loadJsonFromAssets())
}

Disposable

We are going to add a variable to hold a Disposable.

private var loadJsonDisposable: Disposable? = null

We'll get to this in just a moment.

onCreate

Now we need to update our onCreate to utilize the new method. Replace the loadJson block inside onCreate with this:

loadJsonDisposable = loadJson()
    .subscribeOn(Schedulers.newThread())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeBy(
        onSuccess = {
            Log.i(ReadingListApp.TAG, "${it.books.size} books loaded")
            viewAdapter.readingList = it
            saveJson(it)
        },
        onError = {
            Log.e(ReadingListApp.TAG, "Error loading books: ${it.message}", it)
            it.message?.let {
                Snackbar.make(binding.recycler, it, Snackbar.LENGTH_LONG)
                    .show()
            }
        },
        onComplete = {
            Log.w(ReadingListApp.TAG, "Unable to load any books")
        }
    )

For the Snackbar import, you will use com.google.android.material.snackbar.Snackbar.

Admittedly, this code snippet seems longer. It is, however, doing a lot more.

Here, we are using RxAndroid to specify that we want to load the json on a new background thread, but call the onSuccess method on the main UI thread. If we successfully load either of the json files, we log it, update the adapter and save it. If there is an error, we are currently logging it and showing a snackbar at the bottom of the page. If we fail to load any books, but there is no error (we are currently swallowing them after all) then it will just log a warning.

Note that we assign the return value to our loadJsonDisposable?

onDestroy

Override onDestroy

If that variable is set, we want to dispose of it. This allows us to tell the system we no longer care about the results of that file being loaded if we are shutting down.

override fun onDestroy() {
    loadJsonDisposable?.dispose()
    super.onDestroy()
}

saveJson

You'll notice that we are still saving the json on the UI thread. Let's change that.

Looking at our saveJson method, we take a parameter, but don't return anything. What we will want to use for this is Completable which allows us to specify success/error.

private fun saveJson(readingList: ReadingList): Completable {
    return try{
        val writer = FileWriter(File(filesDir, JSON_FILE))
        app.gson.toJson(readingList, writer)
        writer.flush()
        writer.close()
        Completable.complete()
    }catch(e: Exception){
        Log.d(ReadingListApp.TAG, "Failed to save Json to files/$JSON_FILE",e)
        Completable.error(e)
    }
}

We just specify that we are complete or that there was an error (as well as update the return type).

We call this from two places.

onCreate
saveJson(it)
    .subscribeOn(Schedulers.newThread())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe()

We're already logging the failure, so we won't do any extra handling here. Worst case scenario, next time we start the app it starts with the same content it did this time.

onActivityResult

In onActivityResult it's a different story. If we fail to save the changes, then that book won't exist the next time we launch the app. In this case, we will show a snackbar if there is an error, and we will only add the book to the UI if it succeeded.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    // We ignore any other request we see
    if (requestCode == EDIT_REQUEST_CODE || requestCode == NEW_REQUEST_CODE) {
        // We ignore any canceled result
        if (resultCode == Activity.RESULT_OK) {
            // Grab the books from the data
            val source = getBook(data, EditBookActivity.EXTRA_SOURCE_BOOK)
            val edited = getBook(data, EditBookActivity.EXTRA_EDITED_BOOK)
            if (edited != null) {
                // Grab the old reading list since it is read-only
                viewAdapter.readingList?.let { oldReadingList ->
                    // Create a new ReadingList
                    val newReadingList = ReadingList(
                        title = oldReadingList.title,
                        books = oldReadingList.books
                            .toMutableList()
                            .filterNot {
                                it.title == source?.title
                                && it.author == source?.author
                                && it.year == source?.year
                            }.plus(edited)
                            .toList()
                    )
                    // Save the results
                    saveJson(newReadingList)
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeBy(
                            onComplete = {
                                // And update our view
                                viewAdapter.readingList = newReadingList
                            },
                            onError = {
                                it.message?.let {
                                    Snackbar.make(binding.recycler, it, Snackbar.LENGTH_LONG)
                                        .show()
                                }
                            }
                        )
                }
            }
        }
    }
    super.onActivityResult(requestCode, resultCode, data)
}

Now, this introduces an interesting side effect. If it takes a long time to save the file, it would then take a long time for their newly edited book to appear in the list. A better approach would probably be to add it with some indicator that it is pending; then update it once it is complete. We'll leave that as an exercise for you to do at your leisure.

We've changed our method signatures pretty drastically. Maybe we should run our test and androidTest?

Clone this wiki locally