The provided content is an in-depth guide on the basics of Kotlin Coroutines in Android development, covering their definition, setup, and usage, including suspending functions, coroutine scopes, contexts, builders, and job hierarchies.
Abstract
The web content serves as a comprehensive tutorial for understanding and implementing Kotlin Coroutines in Android applications. It begins by defining coroutines as light-weight threads that enable writing asynchronous, non-blocking code sequentially. The guide details the necessary steps to import coroutine libraries, such as kotlinx-coroutines-core and kotlinx-coroutines-android, into an Android project. It then delves into various coroutine components, including suspending functions that can pause execution, different coroutine scopes like CoroutineScope, MainScope, and GlobalScope, and the coroutine context which includes Dispatchers, CoroutineExceptionHandler, and Job. The article also explains coroutine builders such as launch and async, and emphasizes the importance of understanding job hierarchies and the distinction between Job and SupervisorJob. The practical examples and explanations aim to equip developers with the knowledge to effectively manage concurrent operations in Android apps using Kotlin Coroutines.
Opinions
The author emphasizes the importance of coroutines for writing asynchronous code in a more manageable and readable way.
There is a clear distinction made between the different types of coroutine scopes and their appropriate use cases in Android applications.
The author suggests that proper handling of exceptions in coroutines is crucial, highlighting the use of CoroutineExceptionHandler and the behavior of CancellationException.
The concept of SupervisorJob is presented as a way to prevent failure in one child coroutine from affecting others, which is particularly useful in complex applications with multiple coroutines.
The guide advocates for the use of async and await to run concurrent operations, demonstrating how this can reduce the overall time cost compared to sequential execution.
The article encourages readers to explore the official Kotlin Coroutines documentation and other resources for a deeper understanding of the topic.
Kotlin Coroutines in Android — Basics
Learn the basic concepts and usage of Kotlin Coroutines.
What is Coroutines
Essentially, coroutines are light-weight threads. It lets us write asynchronous, non-blocking code in a sequential way.
How to import Kotlin Coroutines in Android?
According to the Kotlin Coroutines Github repo, we need to import kotlinx-coroutines-core and kotlinx-coroutines-android (This library supports for the Android main thread just like the library io.reactivex.rxjava2:rxandroid for RxJava, and also makes sure the uncaught exceptions could be logged before crashing Android application.). Furthermore, if you’re using RxJava in your project, please add kotlinx-coroutines-rx2 for using coroutines with RxJava as well. This library helps to transfer the RxJava to Coroutines.
Import them to your project by adding the following code to app/build.gradle.
First, here is what Coroutines basically looks like:
We fetch the data from the server in the background thread and then update the UI in the main thread.
1. Suspending functions
There’s a special function called suspending functions in Kotlin Coroutines, we can declare the function with keyword suspend. Suspending functions can suspend the execution of a coroutine, which means it will wait until the suspending functions resume. Since this post is for the basic concept of Coroutines, we’ll discuss more detail of suspending functions in this post.
Let’s back to the code snippet above, it could be divided into four parts:
2. CoroutineScope
Defines a scope for new coroutines. Every coroutine builder is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate both context elements and cancellation.
All coroutines run inside a CoroutineScopeand it takes a CoroutineContext (I’ll talk about it later) as a parameter. There are several scopes we may use:
(1) CoroutineScope
Creates the scope with custom CoroutineContext. For example, to define the thread, parent job and exception handler by our need.
(2) MainScope
Creates the main scope for UI components. It is running on the main thread with SupervisorJob(), which means the failure of one of its child job won’t affect others.
(3) GlobalScope
This is a scope which doesn’t bound to any job. It is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.
3. CoroutineContext
Coroutine always executes in some context represented by a value of the CoroutineContext type. CoroutineContext is a set of elements, to define the threading policy, exception handler, control the lifetime of the coroutine, and so on. We can use plus operator to combine the elements of CoroutineContext.
There are three most important Coroutine context — Dispatchers, CoroutineExceptionHandlerand Job.
(1) DispatchersDefines which thread runs the coroutine. A coroutine can switch Dispatchers anytime with withContext().
Dispatchers.Default:
Uses a shared background pool of threads. By default, the maximum number of threads used by this dispatcher is equal to the number of CPU cores, but is at least two. The thread would be like Thread[DefaultDispatcher-worker-2,5,main].
Dispatchers.IO:
Shares threads with Dispatchers.Default, but the number of threads is limited by kotlinx.coroutines.io.parallelism, It defaults to the limit of 64 threads or the number of cores (whichever is larger). The thread would be like Thread[DefaultDispatcher-worker-1,5,main], seems the same as Dispatchers.Default.
Dispatchers.Main:
Equals to Android main thread. The thread would be like Thread[main,5,main].
Dispatchers.Unconfined:
A coroutine dispatcher that is not confined to any specific thread. The coroutine executes in the current thread first and lets the coroutine resume in whatever thread that is used by the corresponding suspending function.
Normally, uncaught exceptions can only result from coroutines created using the launch builder. A coroutine that was created using async always catches all its exceptions and represents them in the resulting Deferred object.
Sample 1: Cannot catch IOException() with outside try-catch.
We cannot wrap the whole CoroutineScope with try-catch, the app will still crash.
Sample 2: Catches IOException() with CoroutineExceptionHandler.
If the exception is other than CancellationException(), for example, an IOException(), it will be propagated to CoroutineExceptionHandler.
Sample 3: CancellationException() is ignored.
If the exception is CancellationException then it is ignored (Because that is the supposed mechanism to cancel the running coroutine, and this exception won’t be propagated to CoroutineExceptionHandler.)
Sample 4: Use invokeOnCompletion to get all exceptions information.
CancellationException() won’t be propagated to CoroutineExceptionHandler. If we want to print some information after the exception happens, we can use invokeOnCompletion to achieve it.
(3) JobControls the lifetime of the coroutine. A job has the following states:
We can simply use Job.isActive to know the current state of a job.
And here is the flow of states change:
A job is active while the coroutine is working.
Failure of the job with exception makes it cancelling. A job can be cancelled at any time with cancel function that forces it to transition to cancelling state immediately.
The job becomes cancelled when it finishes executing its work.
Parent job waits in completing or cancelling state for all its children to complete before finishing. Note that completing state is purely internal to the job. For an outside observer a completing job is still active, while internally it is waiting for its children.
(3.1) Parent-child hierarchies
After realizing the states, we need to know how the parent-child hierarchies work. Let’s say we write the code like this:
val parentJob = Job()
val childJob1 = CoroutineScope(parentJob).launch {
val childJob2 =launch { ... }
val childJob3 =launch { ... }
}
Then the parent-child hierarchies would be like this:
We can change the parent job while launching like this:
val parentJob1 = Job()
val parentJob2 = Job()
val childJob1 = CoroutineScope(parentJob1).launch {
val childJob2 =launch { ... }
val childJob3 = launch(parentJob2) { ... }
}
Then the parent-child hierarchies would be:
Base on the knowledge above, there are many important concepts that we need to know for a Job.
Cancellation of a parent leads to immediate cancellation of all its children.
Failure or cancellation of a child with an exception other than CancellationException immediately cancels its parent and other children. But if the exception is CancellationException, the other jobs not under the cancel job won’t be affected.
If we throw IOException in one of the child jobs, all the relevant job will be cancelled:
cancelChildren(): A parent can cancel its own children (including all their children recursively) without cancelling itself. Note if a job is cancelled, it could not be used as a parent job to run coroutine again.
If we use Job.cancel(), the parent job will start to be cancelled(Cancelling). And after all the child jobs is cancelled, the parent job status will become cancelled.
If we use Job.cancelChildren() instead, the parent job will still be Active. And we can still use it to run other coroutines.
(3.2) SupervisorJob v.s. Job
Children of a supervisor job can fail independently of each other.
As we told before, if we use a simple Job() as the parent job, all children will be cancelled if one of the child jobs fails:
If we use a SupervisorJob() as the parent job, the failure of one child job won’t affect other child jobs:
4. Coroutines Builder
(1) launch :Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job.
(2) async and await :The async coroutine builder is defined as an extension on CoroutineScope. It Creates a coroutine and returns its future result as an implementation of Deferred, which is a non-blocking cancellable future — it is a Job with a result.
Async is used with await: Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete, returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
The following code demonstrates a sequential invocation of two suspending functions. We do some time-consuming task which will cost 1 sec in both fetchDataFromServerOne() and fetchDataFromServerTwo(). And then invoke them in the launch builder. We’ll find the final time cost will be the sum of the time cost: 2 secs.
The log will be:
2019-12-0900:00:34.547 D/demo: fetchDataFromServerOne()
2019-12-0900:00:35.553 D/demo: fetchDataFromServerTwo()
2019-12-0900:00:36.555 D/demo: The sum is 32019-12-0900:00:36.555 D/demo: Completed in 2008 ms
The time cost is the sum of the delay time of two suspending functions. It will suspend until fetchDataFromServerOne() finishes, then execute fetchDataFromServerTwo().
What if we want to run both functions concurrently to reduce the time cost? Async comes to help! Async is pretty much like launch. It starts another coroutines that works concurrently with all the other coroutines, and returns Deferred which is a Job with return value.
publicinterfaceDeferred<out T> : Job {
publicsuspendfunawait(): T
...
}
We can call await() on a Deferred value for the result. For example:
The log will be:
2019-12-0823:52:01.714 D/demo: fetchDataFromServerOne()
2019-12-0823:52:01.718 D/demo: fetchDataFromServerTwo()
2019-12-0823:52:02.722 D/demo: The sum is 32019-12-0823:52:02.722 D/demo: Completed in 1133 ms
5. Coroutine body
The code running in the CoroutineScope, including a regular function or a suspending functions, which will suspend the coroutine until it finishes. We’ll go into detail in the next post.