Coroutines in Android explained
Everything you need to know on coroutines while you wait till your pizza gets delivered.
Implementing coroutines to your android project.
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
What is coroutines?
First you need to know what functions and threads are to make sense of coroutines, functions are a set of instructions that give us outputs based on our inputs,threads describes in which context these sequence of instructions should be executed in. In android if you don't manage threading effectively, every code gets executed in the main thread along with UI , this can cause the infamous ANR crashes/bugs .
This is where coroutines comes to the rescue
- Coroutines are executed within a thread , so many coroutines can be run at a single time without disrupting any other coroutines or the thread in which its running.
- Coroutines are suspendable, meaning any single coroutine can be run,delayed or cancelled at any time.
- And the best part of them is they can change their context at any time.
Example : Imagine 2 construction sites.
Site A = Thread 1
Site B = Thread 2
Constructions workers = Coroutines doing specific tasks
Site A-B Architect = You
breaking it down : In above example any amount of constructions workers(coroutines with tasks) can work in any sites(Threads), If any construction worker is taken out(delayed/cancelled) it will not affect the work of the site, it will carry on as usual, these workers can switch sites(contexts) at any time by the instruction of the architect(you).
The simplest way to start a coroutine , is in the main function(using GlobalScope not the best practice :P )
GlobalScope.launch {your code goes here}
Coroutines started with Globalscope will live as long as the application lives, meaning it will only end if the app is killed or stopped.Globalscope launches the coroutine in a new thread , you can test this out by printing Thread.currentThread().name inside the launch block and outside in the main function, 2 different names will be printed which indicates 2 different threads.
Delay
GlobalScope.launch {
delay(1000L)
}
Delay is NOT SIMILAR to sleep function in threads, delay will only pause the coroutine and not the whole thread.
Suspend Functions
suspend fun doNetworkCall()
What is it used for ? It is used in situations where any function needs to be delayed or is a long running operation , ex: a network call ,these functions can only be invoked by a coroutine or another suspend function
Coroutine Contexts
GlobalScope.launch(Dispatchers.Main) {
//used for UI updates
}
GlobalScope.launch(Dispatchers.IO) {
//used for Network calls
}
GlobalScope.launch(Dispatchers.Default) {
//used for long running operations
}
GlobalScope.launch(Dispatchers.Unconfined) {
//used when you don't want it to be confined to specific thread
}
GlobalScope.launch(newSingleThreadContext("myThread")) {
//used when you want it to run on your thread
}
But the real use of these contexts is to switch between tasks easily, example : in a network call, you can use Dispatchers.IO to make the call and switch to Dispatchers.Main to update the UI based on the requirements,code example given below.
GlobalScope.launch(Dispatchers.IO) {
val response = doNetworkCall()
withContext(Dispatchers.Main){
yourTextView.text = response
}
}suspend fun doNetworkCall():String {
return "response"
}
Run Blocking
runBlocking {
delay(100L)
}
runBlocking is used to suspend or delay the entire thread instead of just the coroutine, why would you do this ? Here are few examples
- If your app has some requirement to block the main thread while performing a task in suspend function.
- To do testing with JUnit to access suspend function from within a test function
- To maybe just play around with coroutines to see how it works
but coroutines started inside the runBlocking will not be blocked and will run asynchronously with the run blocking and finish at the same time as runBlockingrunBlocking {
launch(Dispatchers.IO) {
println("Task 1")
}
launch(Dispatchers.IO) {
println("Task 2")
}
}
Jobs,Waiting and Cancellation
When you launch a coroutine it will return a job, you can assign this to a val , you can then attach another coroutine to the first job and this will allow you to continue tasks sequentially as per your needs, you can delay or cancel it. Below example i am attaching val job to a coroutine and joining it with runBlocking, what this will do is block the thread until the first coroutine is complete and join in from there.
val job = GlobalScope.launch(Dispatchers.Default) {
repeat(2){
Log.d(TAG,"Job coroutine running now")
}
}
runBlocking {
job.join()
Log.d(TAG,"2ND coroutine running now")
}
.cancel() method keyword has the ability to cancel a operation , but .cancel() is a co-operative function which means our coroutines needs to be setup correctly to cancel ,that is why in below example i used delay() since it’s a small task to begin with, you can use delay() and runBlocking{} to your purpose.
val job = GlobalScope.launch(Dispatchers.Default) {
repeat(5){
Log.d(TAG,"Job coroutine running now")
delay(1000L)
}
}
val job2 = GlobalScope.launch {
delay(2000L)
job.cancel()
Log.d(TAG,"Job coroutine is cancelled, Job2 is running now")
}
but then you might wonder , then why do we even need this function?
Coroutines are usually busy running their tasks , so .cancel() on its own won't cancel that particular coroutine, thats where you should you should use the keyword isActive (boolean), which will check the status before running if its been cancelled or not. Example below
GlobalScope.launch(Dispatchers.Default) {
repeat(5){
if (isActive) {
Log.d(TAG, "Job coroutine running now")
}}}
Async and Await
If we have several suspend functions and execute them in a coroutine they are sequential by default, the first function will be executed initially and when its finished the second one will be executed, however when we do network calls we sometimes need them to run concurrently(at the same time).
There are 2 ways to do this
Solution 1 (The bad practice)GlobalScope.launch(Dispatchers.IO) {
val job1 = launch { networkCall1 }
val job2 = launch { networkCall2 }
job1.join()
job2.join()
Log.d(TAG,"Print response : $job1 and $job2")
}Why is this bad practice?
launch is used to execute and forget , if the code inside the launch stops with a execution or error, it is treated as an unhandled/uncaught exception in a thread, and join waits for its completion but does not take responsibility which might result in a crash or ANR.Solution 2(The good practice)GlobalScope.launch(Dispatchers.IO) {
val job1 = async { networkCall1 }
val job2 = async { networkCall2 }
job1.join()
job2.join()
Log.d(TAG,"Print response : $job1.await() and $job2.await()")
}Why is this good practice?
async and await takes responsibility of the task they run and respond accordingly to the final result,they are a instance of the deferred,the result of the function can be retrieved by the await method,no exceptions will be ignored and can be handled using the getCompletionExceptionOrNull.
LifecycleScope and ViewModelScope
To implement these you need to add 2 extra dependencies which are already mentioned above.
Earlier i had mentioned GlobalScope as a bad practice because it will keep running regardless of the activity lifecycle that it was initiated in, this creates a lot of memory leaks because GlobalScope will stop only when the application is destroyed. If the Activity which provides resources used to create the GlobalScope is destroyed , it won't be garbage collected because the resources are still being used.
To prevent the above mentioned horror show we use lifecycleScope, this will run the coroutine in accordance to the activities lifecycle.lifecycleScope.launch {
doSomething()
}Another useful scope is the viewModelScope, this does the same function as mentioned above only difference is it will run according to the viewmodel lifecycle, viewmodels are used in MVVM architecture recommended by google.viewModelScope.launch {
doSomething()
}
Coroutines with Retrofit
The usual way of making retrofit calls is done using the enqueue function and result is returned through the callback method, but enqueue creates a whole new thread and this isn't efficient. We can instead execute this inside a coroutine.
interface MyAPI{
@GET("url")
suspend fun getApi():Response<YourModelClass>
}MainActivity {
lifecycleScope.launch {
val response = api.getApi()
if (response.isSuccessful){
//do something here
}
}}This is just a simple example to show how much less code it is to call an api through coroutines and its not needed to start a thread for it.
I hope this article helped you understand coroutines and this was enough to get you started, in the meantime enjoy your pizza.