In this lesson, you’ll implement a simple program that demonstrates the concept of concurrency.
Open the starter project in Android Studio and navigate to the Lesson1Screen.kt
file. The skeleton of the program is already prepared for you. You only need to fill in the places marked with TODO
comments.
The demo app is a simple Android app written in Kotlin using the Jetpack Compose library. It consists of a screen with several buttons. Each button starts a different task. Most of the results are printed
to Logcat. To speed up development, you can enable the Show logcat automatically
option in the
run configuration. To do that, open the Run/Debug Configurations
dialog. Then, in the
Miscellaneous
tab, check all the options in the Logcat
section.
Don’t forget to click the Apply
button to save the changes. Before you start coding, take a look at two different approaches to concurrency in Android. They were popular before the introduction of Kotlin Coroutines.
RxJava
The first one is RxJava. It’s a library that implements reactive extensions and the observer pattern. An observer is a callback invoked when a certain event occurs. RxJava is still under active development. Newer versions are released regularly. Many existing Android app projects use RxJava.
Note, as the name suggests, RxJava is written in Java, not in Kotlin. You can use its APIs from Kotlin. But, you’ll observe no such features like named arguments. There’s a separate library called RxKotlin that provides Kotlin extensions. But the latter isn’t under active development. There’s also a tiny library called RxAndroid. It provides Android-specific features related to the main thread.
Open Lesson1Screen.kt
. Look at the code displaying the current time on top of the Column
. This is a mutable state of String containing the current, formatted time. All the logic is inside the DisposableEffect
block, which is a lambda that runs when the composable enters the composition for the first time and each time the key changes. In this case, the key is always the same, so the lambda runs only once. The lambda has the second, onDispose
lambda inside, which runs when the composable leaves the composition and also when the key changes.
It’s important to clean up resources when they’re no longer needed. Updating the UI after the user leaves the screen leads to leaking resources.
The core of the logic is the Observable.interval
method. It emits a value every 100 milliseconds. The value here is the Long
starting from 0 and incrementing by 1 on each emission. It returns the Observable<Long>
. An observable is a stream of values, similar to the Flow
in Kotlin Coroutines, which you’ll learn about in the next lesson. You can subscribe to it to receive values.
Run the app and observe the time changing on the screen. Next, see the Logcat panel. You’ll find the Time tick #<n>
messages approximately every 100 milliseconds. If the messages are too frequent for you to read, you can pause the Logcat output by clicking the Pause
button on the left side of the panel. <n>
is the consecutive number of ticks, starting from 0. The source of the messages printed to Logcat is the onNext
operator. It intercepts the values emitted by the Observable
.
Why do messages appear approximately, but not exactly, every 100 milliseconds? Well, that’s because there’s an overhead in the system. Creating a message and sending it to the Logcat internals takes some time.
Next, there’s a map
operator. It transforms each value emitted by the Observable
into
another one. In this case, the incoming value isn’t meaningful. The output is the current formatted
time.
After the map
operator, there’s a observeOn
operator. It specifies the scheduler on which all of the following observers will run. The scheduler defines a thread pool. It’s the equivalent of
the Dispatcher
in Kotlin Coroutines.
You may change schedulers as many times as you want at any point in the chain using the multiple observeOn
operators. There’s also the subscribeOn
operator. It specifies the scheduler for all the operators, both before and after it. Only one subscribeOn
operator makes sense in the chain.
Some initial operators like interval
run, by default, on a particular scheduler. You may change that scheduler using their optional, last parameters. A subscribeOn
will also change those default schedulers. Finally, there are initial operators which don’t specify any scheduler. If there’s no subscribeOn
in the chain, they run on the caller thread.
The AndroidSchedulers.mainThread()
is a scheduler that runs the observers on the Android main thread. There are also other schedulers available in RxJava, like Schedulers.io()
dedicated to Input/Output
operations (e.g. network requests) or Schedulers.computation()
for CPU-intensive operations.
Finally, there’s the subscribe
operator. It’s at the end of the chain. It doesn’t produce any output to the chain. It only consumes the items emitted by the Observable
. The subscribe
operator returns the Disposable
object. It’s a handle to the subscription. You can use it to dispose of the subscription.
It’s important to dispose of the subscription when it’s no longer needed. There’s also the possibility to subscribe to the error and completion events. There are optional lambda parameters to the subscribe
operator. The simple timer doesn’t produce any expected exceptions. There are no operations like network requests that may fail.
Moreover, the timer never completes. It emits the items infinitely until you dispose of the subscription. Thus, the error and completion lambdas are missing.
AsyncTask
A second approach is AsyncTask
. It’s a class from the Android SDK, and doesn’t require any external libraries. Take a look at the end of the Lesson1Screen
file. There’s an AsyncTaskExample
class. It extends a generic AsyncTask
class. The three type parameters represent the input, the progress, and the result.
To start the AsyncTask
create an instance of the class and call the execute
method:
AsyncTaskExample().execute()
Now, click the Start AsyncTask
button and observe in Logcat which thread the methods run on. You may want to comment out the code printing the time ticks to avoid the clutter in the Logcat.
The AsyncTask
is a simple way to perform background operations. It’s much less powerful than RxJava or Kotlin Coroutines. It provides neither the operators to transform the data or the ability to handle
errors. The doInBackground
method, by default, runs on the background thread. The default background executor runs each task serially, one by one. You can, however, change the executor to another one.
There is the ability to cancel the task. But, you need to check the isCancelled
flag in the doInBackground
method. All other methods run on the main thread. The proper implementation of the typical business logic requires a lot of boilerplate code. That’s why the AsyncTask
has been deprecated. You shouldn’t use it in new projects. But, it will be available in the Android SDK for the foreseeable future for backward compatibility.
Blocking the Main Thread
OK, now it’s time to start coding. You have two tasks to implement. In the first one, you’ll implement the long running operation on the main thread. Look at the “Do blocking work on main thread” button. There’s also a doBlockingWork
method which simulates some long running operation. In fact, it sleeps the caller thread for three seconds.
How do you block the main thread using that method? Well, you have to invoke it on the main thread. But how do you get the main thread? Do you remember that the main thread is the one where all the UI operations occur? So, it’s a thread on which the onClick
method runs. Add the doBlockingWork
method there. The code should look like this:
Button(onClick = {
doBlockingWork()
}) {
Text(text = "Do blocking work on main thread")
}
Run the app and click the button.
Notice that the app freezes for three seconds. The timer pauses, and the buttons aren’t clickable or even focusable. If you click the button several times in a row, you’ll get the “Application Not Responding” dialog. It’s a system dialog that appears when the main thread is blocked for more than three seconds. The system assumes that the app is frozen and offers the user the ability to force close it.
Five seconds is the threshold imposed by the Android system. But even 100 milliseconds is too long if you want to have a good user experience. Smooth animations require 60 frames per second, which means 16 milliseconds per frame. That time includes all the internal operations performed by the framework to render the UI, and not just the time spent in your code. Try lowering the interval of the timer and check how smoothly the time changes.
Background Thread
In the second task, you’ll implement the long running operation on the background thread. You already know that the main thread should be responsive. So, the long operations should run on background threads. Look at the “Do blocking work on background thread” button.
To run something on the background thread, you need that thread first. You can spawn one using thethread
function. It’s a top-level function from the Kotlin standard library. It takes a lambda, which will execute on the background thread. The code should look like this:
Button(onClick = {
thread {
doBlockingWork()
}
}) {
Text(text = "Do blocking work on background thread")
}
You’ll need to choose the import from the kotlin standard library. Run the app, click the button and observe the behavior. The timer shouldn’t pause, and the buttons are clickable all the time.
Note that this example is only for illustration purposes. In real-world applications, things like spawning new threads inside the onClick
method aren’t good ideas. Creating a new thread is a costly operation. You should use some kind of thread pool to take the thread from.
Notice the Thread in Android is low-level construct. Normally, in Kotlin Coroutines, you’ll operate on a higher level of abstraction. For example, Dispatchers for distributing background work.
There’s one more issue with the thread created in the onClick
handler. It keeps running even if the user leaves the screen. In a real-world application, you should cancel the background work when
it’s no longer needed. You can use coroutine scopes for that. You’ll learn about them in the following lessons.