Coroutine in Android: Basic
In this series of blog posts, I am going to explore Kotlin Coroutine and it’s usage in the Android world.
- Coroutine in Android: Basic (You are here)
- Coroutine in Android: Working with Lifecycle
- Coroutine in Android: Working with Retrofit (credit: zsmb13)
- Coroutine in Android: Working with Room Database
- Coroutine in Android: Working with Workmanager
Android development is evolving rapidly. Android development with java was full with so much boilerplate code. By the introduction of Kotlin, now we can be more focused on business logic. Same goes to the Kotlin coroutine. Before coroutine, we had to write so much boilerplate code to run asynchronous business logic. Now with a coroutine, we can write asynchronous and non-blocking code as if it was synchronous and blocking code.
What is Kotlin Coroutine
“A coroutine is an instance of suspendable computation, conceptually similar to a thread, in the sense that it takes a block of code to run and has a similar life-cycle, it is created and started, but it is not bound to any particular thread. It may suspend its execution in one thread and resume in another one. Moreover, like a future or promise, it may complete with some result or exception.”
Kotlin coroutine is a lightweight thread. Coroutines are way cheaper than actual Java Thread. So a developer can fire and forget a long number of coroutine without any headache. Coroutines are more like a task that can be executed in any thread.
Coroutine is a Kotlin language feature. But Kotlin standard library provides only minimal low-level APIs by which other libraries can utilize Coroutine feature. kotlinx.coroutines is a rich library for coroutines, developed by JetBrains. It contains a number of high-level coroutine-enabled primitives. If you want to add kotlinx.coroutnes in your project add the following gradle dependencies:
dependencies {
...
def coroutines_version = "1.3.0-M2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
...
}
suspend is the main keyword you will encounter to work with Kotlin Coroutine. suspend keyword is provided by the Kotlin standard library. Let’s discuss the keyword
What is suspend
suspend is a Kotlin keyword which can be used before any kind of function. Any function marked with suspend keyword known as suspending function. Long-running (blocking) suspending function can be suspended from a thread and after completing the task on other thread can be resumed later on the caller thread.
In the example above, get(…) will suspend the Coroutine before it starts the network request. The function get(...) will still be responsible for running the network request off the main thread. Then, when the network request completes, instead of calling a callback to notify the main thread, it can simply resume the Coroutine it suspended.
Enough talk. Now let’s write code with suspend and Coroutine.
Here delay(...) is a suspending function that operates the same as Thread.sleep(...). There is also an indication on the left of delay(millis) that this is a suspending function.
Calling Suspending Function
Here if we remove suspend keyword from logLater(…) function then we can’t call delay(...). Because delay(...) is a suspending function and it is illegal to call suspending function from any normal function. Suspending functions can only be called from other suspending function and from Coroutine. To bridge the non-blocking world with blocking world we have to create Coroutine using coroutine builder. The most used coroutine builder is launch(...) function which is an extension function of CoroutineScope class.
Suspending functions can only be called from other suspending function and from Coroutine.
Changing Thread in Coroutine
Suspending function does not change Thread automatically. It is our responsibility to change Thread when we need. But in the Coroutine world, we don’t use Thread directly. We use CoroutineDispatcher to tell Coroutine to change the Thread. CoroutineDispatcher is responsible to determine what thread the corresponding coroutine uses for its execution.
The lunch(..) function takes an optional CoroutineDispatcher parameter to change the Thread.
We can also change the Dispatcher inside any suspending function using withContext(...) function. withContext(...) function returns the value of the last expression.
There are Three CoroutineDispatcher in the kotlinx.Coroutine library. They are:
+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------++-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------++-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+
In Android, you normally interact with the UI which can’t be modified from other thread. So I recommend using Dispatchers.Main in the Coroutine builder. And if you need to change Thread then do this using withContext(...) inside Coroutine or any suspending function. And try not to touch UI elements from suspending function. Try to work with UI elements only from parent Coroutine.
Concurrency in Coroutine
Suspending functions are sequential by default. The codes inside suspending functions and Coroutine get executed one after another & sequentially. If we see the logLater(...) suspending function, at first the first log get executed then it waits for the delay and then the second log gets executed. Everything in suspending world is sequential unless we tell explicitly to run the code concurrently.
To run code concurrently we need to use the async(...) builder method. The async(...) returns Deferred object and by using this object we can wait for the result. async(...) is an extension function on CoroutineScope class.
If we see the log message we can find that without concurrency it took 10 seconds to complete the task but with async(...) function and explicit concurrency it took only 6 seconds to complete.
Notice: coroutineScope{...} block has been used to call async(...) inside a suspending function.
async(...) can be called in many ways but it is recommended to call it only inside lunch{..} or coroutineScope{...} block.
CoroutineScope
By this time you should have noticed CoroutineScope multiple time. I have used GlobalScope to launch new coroutine which is an implementation of CoroutineScope. Now it is high time to learn about this.
I think of CoroutineScope as a boundary of the coroutines. CoroutineScope helps us to maintain the life-cycle of the coroutines. It has some extension functions by those we can easily create and cancel coroutines.
As of now, we have seen two coroutine builders launch(...) and async(...). Both of them are defined as extension function of CoroutineScope class to encourage developers to structure their coroutines.
fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
To cancel coroutine we have an extension function:
fun CoroutineScope.cancel(...): Unit {...}
Structured Concurrency
If you have read some blog posts or viewed some videos explaining Coroutine you most probably heard about the term “Structured Concurrency”.
To me, Structured Concurrency is the coding paradigm; it is the style of writing coroutine so that we have a minimum headache to maintain and understand the life-cycle of coroutine in our codebase.
If you are using any CoroutineScope other than GlobalScope and you call async(...) only inside launch{...} or coroutineScope{...} block then you are writing coroutines with Structured Concurrency. And make sure you do not have Deferred object outside of coroutine.
Using GlobalScope may lead to a memory leak.
In the above example, I have used GlobalScope for simplicity. But in structured concurrency, you are highly discouraged to use GlobalScope. Using GlobalScope may lead to a memory leak. Luckily in Android, we don’t need to write our own custom CoroutineScope because Android Jetpack already provides us all the CoroutineScope we need. In the second blog post I am going to write about this.
Visit here to learn more about Structured Concurrency.
Coroutine API Annotations
While we are familiar with Alpha, Beta and Stable release, the kotlinx.coroutines library takes a different approach to define its API stability. There are some annotations that indicate the state of the API.
+-----------------------------------+
| ExperimentalCoroutinesApi |
+-----------------------------------+
| there is a chance that those |
| declarations will be deprecated |
| in the near future |
+-----------------------------------++-----------------------------------+
| ObsoleteCoroutinesApi |
+-----------------------------------+
| these declarations will be |
| deprecated in the future but |
| there is no replacement for |
| them yet |
+-----------------------------------++-----------------------------------+
| InternalCoroutinesApi |
+-----------------------------------+
| should not be used outside of |
| `kotlinx.coroutines` |
+-----------------------------------++-----------------------------------+
| FlowPreview |
+-----------------------------------+
| Marks [Flow]-related API as a |
| feature preview Flow preview |
| has **no** backward compatibility |
| guarantees |
+-----------------------------------+
There is also an interesting twitter thread about Coroutine API annotations which you may find helpful.