« Mastering Coroutines in Kotlin
Main thread
When the user launches the application, a default thread is created, and this thread is known as the “Main Thread.”
This main thread is the life of the application.
And on this main thread, we perform small operations, such as :
- UI interactions
- Click the button
- Mathematical operations
- Small logical operations
Operations that require the use of the internet, uploads or downloads of a file, loading and displaying an image from the server, or running some heavy database queries. If you run these heavy operations on the same main thread, your main thread will be blocked, and then your application will freeze. If your main thread remains blocked for a longer time, for example, 5 seconds or something, eventually, your application will shut down. With a crash message.
So, running such heavy operations on the main thread is not good.
One solution to this issue is that we can create a worker thread or a background thread. And on this worker thread, we can run heavy operations.
- We can create a thread to perform some network operations.
- We can create another background thread to download or upload a file.
- We can launch more threads to load images or to perform some heavy database queries.
So, in general, we can always create a new thread to perform some operations without affecting your main thread.
It seems like a good solution! But there is a limit to how many background threads you can create at a time. Because creating threads for each operation really is not good. Because if you create more and more threads, then there will be a time when your device will run out of memory. And once again, your app will shut down.
Fundamentals of coroutine
When you use coroutines, you don’t have to create so many threads for each operation. You can have just one background thread, and you can launch coroutines on this thread.
- Launching a coroutine to perform file upload.
- Launch another coroutine on the same background thread to perform some network operations
- Launch one more coroutine to download a file
We can also have multiple coroutines performing different tasks.
You can perform so many heavy tasks with the memory consumption of one background thread.
This is what a coroutine does.
Why Use Coroutines?
Android developers have many async tools available today.
If you’ve worked with Rx, then you know it takes a lot of effort to get to know it well enough to be able to use it safely. AsyncTasks and threads are easily capable of causing leaks and extra memory usage.
Finally, using all these APIs can introduce a ton of code since they use callbacks. Also, adding more callbacks can make our code unreadable.
Once you try Kotlin Coroutines, you’ll realize they’re not just another tool. It’s a totally new way to think about asynchronicity!
Coroutines demo
main function:
1fun main() {23}
Basically, this main function runs on the main thread. For example, I put some print statements here.
1fun main() {2 println("${Thread.currentThread().name} -> thread has run.")3}
Then this print statement will also be executed in the main thread. I am going to print the thread name. As a result, you can see :
main -> is running
The thread’s name is MAIN.
And now, let me create a background thread that is also known as a worker thread.
1fun main() {2 val thread = Thread {3 println("${Thread.currentThread().name} -> is running")4 }5 thread.start()6}
Thread-0 -> is running
Let’s create our first coroutine:
1fun main() {2 runBlocking {3 launch {4 delay(1000L)5 println("World!")6 println("${Thread.currentThread().name} -> is running")7 }8 println("Hello")9 println("${Thread.currentThread().name} -> is running")10 }11}
You will see the following result:
Hello
main -> is running
World!
main -> is running
runBlocking
is a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking { … }
launch
is a coroutine builder as well. It launches a new coroutine concurrently with the rest of the code, which continues to work independently. That’s why “Hello” was printed first.
delay
is a special suspending function. It suspends the coroutine for a specific time. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.
What are the suspended functions?
A suspend modifier was added to the coroutine code. Doing this informs the compiler that this function needs to be executed inside a coroutine. Suspend functions can be viewed as normal function execution that is sometimes suspended and resumed. The suspending functions are only allowed to be called from a coroutine or from another suspending function. In short, they cannot be called from outside a coroutine.
What are coroutine builders?
Coroutine builders are methods for creating coroutines. Since they are not suspending themselves, they can be called non-suspending code or any other piece of code. They act as a link between the suspending and non-suspending parts of our code.
Three important types of coroutine builders:
runBlocking
launch
async
A coroutine can be created in many different ways, but these are the most important.
runBlocking
When a coroutine is constructed with “runBlocking”, the current thread is blocked until all tasks in the coroutine have been completed.
To test suspending routines, we typically use runBlocking. During test execution, it is important to avoid completing the test while performing intensive work in the test suspend routines.
1fun main() {2 runBlocking {3 delay(1000L)4 println("World!")5 println("${Thread.currentThread().name} -> is running")6 }7 runBlocking {8 delay(1000L)9 println("World!")10 println("${Thread.currentThread().name} -> is running")11 }12 runBlocking {13 delay(1000L)14 println("World!")15 println("${Thread.currentThread().name} -> is running")16 }17 println("Hello, ")18}
Result:
World!
main -> is running
World!
main -> is running
World!
main -> is running
Hello,
Use in Test Class :
1class MyTests {2 @Test3 fun `a test`() = runBlocking {4 }5}
launch
launch
is a coroutine builder that creates a new coroutine without returning any results. It can also start a coroutine in the background.
The launch function is an extension function on the CoroutineScope interface. This is part of an important mechanism called structured concurrency, whose purpose is to build a relationship between the parent coroutine and a child coroutine.
1fun main() {2 GlobalScope.launch {3 delay(1000L)4 println("World! in 1st launch")5 println("${Thread.currentThread().name} -> is running in 1st launch")6 }7 GlobalScope.launch {8 delay(1000L)9 println("World! in 2nd launch")10 println("${Thread.currentThread().name} -> is running in 2nd launch")11 }12 GlobalScope.launch {13 delay(1000L)14 println("World! in 3rd launch")15 println("${Thread.currentThread().name} -> is running in 3rd launch")16 }17 println("Hello,")18 Thread.sleep(2000L)19}
Result :
Hello,
World! in 2nd launch
World! in 1st launch
DefaultDispatcher-worker-2 -> is running in 1st launch
World! in 3rd launch
DefaultDispatcher-worker-1 -> is running in 3rd launch
DefaultDispatcher-worker-3 -> is running in 2nd launch
Async (async-await)
Async is a coroutine builder similar to launch, but it returns some value to the caller. Using the async method, you can perform an asynchronous task that returns a value. In Kotlin, this is called a Deferred value.
When await() is applied to get the value of the delivered deferred object, the async builder is suspended. Async will pause the currently running coroutine until the outcome has been determined. The coroutine continues to run as soon as the result is returned.
1fun main() {2 runBlocking {3 val resultDeferred = async {4 delay(1000L)5 println("${Thread.currentThread().name} -> is running")6 867 }8 val result = resultDeferred.await()9 println(result)10 println(resultDeferred.await())11 println("${Thread.currentThread().name} -> is running")12 }13}
Result:
main -> is running
86
86
main -> is running
Dispatchers
You can specify the coroutine context when you start a coroutine. The coroutine context has a coroutine dispatcher that chooses the thread or threads that will be used by the related coroutine to execute. The dispatcher for the new coroutine and other context elements can be explicitly specified using the optional CoroutineContext parameter, which is accepted by all coroutine builders, including launch and async.
Types of Dispatchers
There are basically four types of dispatchers:
- Dispatchers.Main
- Dispatchers.Default
- Dispatchers.IO
- Dispatchers.Unconfined
Dispatchers.Main:
A coroutine dispatcher that is confined to the Main thread operating with UI objects. Usually, such a dispatcher is single-threaded.
1fun main (){2 runBlocking {3 launch {4 println("${Thread.currentThread().name} -> is running")5 }6 }7}
Result:
main -> is running
Dispatchers.Default:
The default CoroutineDispatcher that is used by all standard builders like launch, async, etc. if no dispatcher nor any other continuation interrupter is specified in their context.
1fun main() {2 runBlocking {3 launch(Dispatchers.Default) {4 println("${Thread.currentThread().name} -> is running")5 }6 }7}
Result:
DefaultDispatcher-worker-1 -> is running
Dispatchers.IO:
The CoroutineDispatcher is designed for offloading blocking IO tasks to a shared pool of threads.
1fun main() {2 runBlocking {3 launch(Dispatchers.IO) {4 println("${Thread.currentThread().name} -> is running")5 }6 }7}
Result:
DefaultDispatcher-worker-1 -> is running
Dispatchers.Unconfined:
A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of the coroutine in the current call frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function without mandating any specific threading policy.
1fun main() {2 runBlocking {3 launch(Dispatchers.Unconfined) {4 println("${Thread.currentThread().name} -> is running")5 }6 }7}
Result:
main -> is running
Difference between IO and Default Dispatcher
Default Dispatcher is preferred for operations you want to off-load from the main thread like CPU-intensive operations.
but
IO Dispatcher is preferred for heavy IO operations like read/write files, uploading or decrypting/encrypting files.
Dispatchers.IO has a unique property of elasticity, which means its pool is not limited to 64 as we can decide to limit it to as many threads as we want.
CoroutineScope
Scopes in Kotlin Coroutines are very useful because we need to cancel the background task as soon as the activity is destroyed.
Assuming that our activity is the scope, the background task should get canceled as soon as the activity is destroyed.
When we need the GlobalScope, which is our application scope, not the activity scope, we can use the GlobalScope as below:
1class MainActivity : AppCompatActivity() {2 override fun onCreate(savedInstanceState: Bundle?) {3 super.onCreate(savedInstanceState)4 setContentView(R.layout.activity_main)56 GlobalScope.launch {7 val userOne = async(Dispatchers.IO) { fetchFirstUser() }8 val userTwo = async(Dispatchers.IO) { fetchSecondUser() }9 }1011 }121314 fun fetchFirstUser() {15 println("fetchFirstUser")16 }1718 fun fetchSecondUser() {19 println("fetchSecondUser")20 }21}
Here, even if the activity gets destroyed, the fetchUser functions will continue running as we have used the GlobalScope.
CoroutineContext
Coroutines are always executed in some context.
The coroutine context is a set of various elements. The main elements are the Job of the coroutine and its dispatcher.
All coroutine builders like launch and async accept an optional CoroutineContext parameter that can be used to explicitly specify the dispatcher for the new coroutine and other context elements.
1class MainActivity : AppCompatActivity() {2 override fun onCreate(savedInstanceState: Bundle?) {3 super.onCreate(savedInstanceState)4 setContentView(R.layout.activity_main)56 runBlocking {7 launch { // context of the parent, main runBlocking coroutine8 Log.i(9 "MainActivity",10 "main runBlocking : I'm working in thread ${Thread.currentThread().name}"11 )12 }13 launch(Dispatchers.Unconfined) { // not confined -- will work with main thread14 Log.i(15 "MainActivity",16 "Unconfined : I'm working in thread ${Thread.currentThread().name}"17 )18 }19 launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher20 Log.i(21 "MainActivity",22 "Default : I'm working in thread ${Thread.currentThread().name}"23 )24 }25 }2627 }28}
Result:
Unconfined : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
main runBlocking : I'm working in thread main
Coroutine cancellation
You need to cancel coroutine when it’s no longer needed.
Calling cancel
When launching multiple coroutines, keeping track of them or canceling each individually can be a pain. Rather, we can rely on canceling the entire scope coroutines are launched into as this will cancel all of the child coroutines created:
1val job1 = scope.launch {… }2val job2 = scope.launch {… }3scope.cancel()
Sometimes you might need to cancel only one coroutine. Calling job1.cancel
ensures that only that specific coroutine gets canceled, and all the other siblings are not affected.
1val job1 = scope.launch {… }2val job2 = scope.launch {… }3job1.cancel()
A canceled child doesn’t affect other siblings.
Exception Handling
Exception handling is another important topic.
When Using launch, one way is to use a try-catch block:
1class MainActivity : AppCompatActivity() {2 override fun onCreate(savedInstanceState: Bundle?) {3 super.onCreate(savedInstanceState)4 setContentView(R.layout.activity_main)5 GlobalScope.launch(Dispatchers.Main) {6 try {7 fetchUser()8 } catch (exception: Exception) {9 Log.i("MainActivity", "$exception handled !")10 }11 }12 }1314 private fun fetchUser() {15 println("fetchUser")16 }17}
Another way is to use a handler:
For this, we need to create an exception handler like the below:
1class MainActivity : AppCompatActivity() {23 val handler = CoroutineExceptionHandler { _, exception ->4 Log.i("MainActivity", "$exception handled !")5 }67 override fun onCreate(savedInstanceState: Bundle?) {8 super.onCreate(savedInstanceState)9 setContentView(R.layout.activity_main)10 GlobalScope.launch(Dispatchers.Main + handler) {11 fetchUser()12 }13 }1415 private fun fetchUser() {16 println("fetchUser")17 }18}
Job In Coroutine
Whenever a new coroutine is launched, it will return a job. The job which is returned can be used in many places like it can be used to wait for the coroutine to do some work, or it can be used to cancel the coroutine. The job can be used to call many functionalities like the join() method, which is used to wait for the coroutine, and the cancel() method, which is used to cancel the execution of the coroutine.
1class MainActivity : AppCompatActivity() {23 override fun onCreate(savedInstanceState: Bundle?) {4 super.onCreate(savedInstanceState)5 setContentView(R.layout.activity_main)6 val job1 = GlobalScope.launch {7 coroutineScope {8 val job2 = launch {9 Log.i("MainActivity", "job2 Started")10 doSomething()11 }1213 job2.invokeOnCompletion { Log.d("MainActivity", "job2 Finished") }14 }15 }16 }1718 private fun doSomething() {19 println("doSomething")20 }21}
Result :
job2 Started
job2 Finished
join() Method
join() function is a suspending function. It can be called from a coroutine or from within another suspending function. Job blocks all the threads until the coroutine in which it is written or has context finished its work. When the coroutine finishes, lines after the join() function will be executed.
1class MainActivity : AppCompatActivity() {23 override fun onCreate(savedInstanceState: Bundle?) {4 super.onCreate(savedInstanceState)5 setContentView(R.layout.activity_main)6 val job = GlobalScope.launch(Dispatchers.Default) {7 repeat(5)8 {9 Log.i("MainActivity", "Coroutines is still working")10 delay(1000)11 }12 }13 runBlocking {14 job.join()15 Log.i("MainActivity", "Main Thread is Running")16 }17 }18}
Result:
Coroutines is still working
Coroutines is still working
Coroutines is still working
Coroutines is still working
Coroutines is still working
Main Thread is Running
cancel() Method
cancel() method is used to cancel the coroutine without waiting for it to finish its work. It can be said that it is just opposite to that of the join method, in the sense that join() method waits for the coroutine to finish its whole work and block all other threads, whereas the cancel() method when encountered, kills the coroutine i.e. stops the coroutine.
1class MainActivity : AppCompatActivity() {23 override fun onCreate(savedInstanceState: Bundle?) {4 super.onCreate(savedInstanceState)5 setContentView(R.layout.activity_main)6 runBlocking {7 val job1 = launch {8 delay(3000L)9 Log.i("MainActivity", "Job1 Started")1011 }12 job1.invokeOnCompletion { Log.i("MainActivity", "Job1 Finished") }13 delay(500L)14 Log.d("MainActivity", "Job1 will be canceled")15 job1.cancel()16 }17 }18}
Result:
Job1 will be canceled
Job1 Finished
Now Log.i(TAG, “Job1 Started”)
is not executed anymore because we are delaying half a second, and then we are canceling it.
For full working code please visit