In this demo, you’ll see how coroutines look in action. You’ll also learn how to start coroutines in your Android applications.
Open the Starter project in Android Studio and look at the Lesson2Screen file. In the LaunchedEffect section,
there’s a timer working in the same way as in the previous lesson. But, this time it uses coroutines
under the hood instead of the RxJava.
The composable state holding the current time and the Text composable displaying it are the same as in Lesson1Screen. The difference is in the logic of the timer. At a glance, it’s much more compact and easier to understand. There’s two times less code than in the RxJava version.
Run the app, and observe the timer. It should work the same way as in the previous lesson. It also logs the time ticks to logcat just like previously.
Note that the timer uses the LaunchedEffect composable instead of DisposableEffect. The meaning of the key1 argument is the same as in DisposableEffect. If the key changes, the LaunchedEffect cancels the previous coroutine and starts a new one.
The lambda of LaunchedEffect is a suspending function. You can call suspending functions from it.
The receiver of the lambda is the CoroutineScope. The LaunchedEffect starts the coroutine while entering the
composition and cancels its scope while leaving the composition. You don’t have to worry about handling the cancellation and disposing of the CoroutineScope. There’s much less boilerplate code, and you can focus on the business logic!
There’s one suspension point in the lambda of the LaunchedEffect. It is the delay function call.
It suspends the coroutine for the given amount of time. Note that you can pass in a kotlin.time.Duration
as an argument. 100.milliseconds in a single expression is more readable than two
separate arguments 100 and TimeUnit.MILLISECONDS in the Observable.interval from RxJava.
Note that there is an infinite loop in the LaunchedEffect:
while (true) {
//..
}
It may look like the coroutine will run forever. But that’s not the case. Once the coroutine is cancelled,
which happens when the LaunchedEffect is removed from the composition, the delay function throws a
CancellationException and no more iterations of the loop will run.
All of the coroutine runs on the Android main thread. It’s the default thread for the LaunchedEffect. Note
the delay suspends the coroutine without blocking the thread. During the suspension, the main thread
is free to do other work. It can process user input, update the UI, or run other coroutines. The app
doesn’t freeze during the suspension period. There are no time-consuming operations in the coroutine, like
network requests or operations on the file system. So it’s safe to run it on the main thread.
OK, now’s the time to implement your first coroutine! Before you start, you may want to comment out the time ticks logging in the LaunchedEffect lambda to avoid cluttering logcat.
Look at the TODO comment inside the “Do suspendable work on main thread” button onClick lambda.
The suspending function simulating some time-consuming work is already there. You have to call it.
But you can’t call the suspending function directly from the regular function. The onClick lambda
isn’t a suspending function. You have to start a coroutine there first.
You have to choose the correct coroutine builder. First, try the runBlocking:
runBlocking {
doSuspendableWork()
}
And add the runBlocking import. Run the app and click the first button, “Do suspendable work on main thread”. You’ll see that the app freezes. As the name suggests, the runBlocking blocks the caller thread. In this case, it’s the main thread. So this builder isn’t suitable here.
Now, try the launch builder. You need a CoroutineScope for it. There is one scope for you on
top of the Lesson2Screen file. You’ll learn more about scopes in the following lessons.
Jetpack Compose provides the rememberCoroutineScope composable function. Note that you can only call it from a composable function. The onClick lambda is not composable. So the rememberCoroutineScope invocation is in the top-level Lesson2Screen() function. That scope follows the lifecycle of the Lesson2Screen composable. It gets cancelled when Lesson2Screen finishes its composition.
Add the launch builder to the onClick lambda:
job = coroutineScope.launch {
doSuspendableWork()
}
and add the launch import. Note the job variable; you’ll use it later to cancel the coroutine.
Rebuild and run the app and then click the first button. Now the app doesn’t freeze. Look at the logcat.
The messages from the doSuspendableWork function are there. The coroutine runs on the main thread.
Now, press the first button quickly multiple times. You’ll see interleaved messages from the different coroutines. Note that each coroutine has its own number.
Click the first button and then immediately click the second one, “Cancel coroutine job.” You should see in the logcat that the message about the work starting is there, but the message about the end of the work is not. The coroutine was cancelled before reaching it. Note that there is also a cancellation exception in the logcat.
Finally, comment out the exception rethrowing in the catch block. Run the app and repeat the button-clicking sequence. Now, the message about the end of the work is there. But it appears immediately after the message about the exception. The delay was interrupted by the cancellation, so the work finished earlier than the delay time.
That’s it for the demo. You’ve seen how to start a coroutine and how to cancel it. You’ve also seen how to handle the cancellation exception.