Coroutines in Android

Overview

With the help of coroutines we can execute multiple task on a single thread. Coroutines are executed in a thread. One thread can have multiple coroutines. Coroutines run on top of Threads. 

Dependency

 def coroutines_version = "1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
Basis of Coroutine
  1. Coroutine Scope - Defines the lifetime of a coroutine
  2. Coroutine Context - Defines the thread which are used to launch a coroutine
Dispatchers are a way to define threads on which Coroutines are executed. Dispatchers dispatch your Coroutines on a Thread. There are some predefined dispatchers:
  1. Dispatchers.IO
  2. Dispatchers.Main
  3. Dispatchers.Default
There are three scopes
  1. CoroutineScope - Use this scope in custom class.
  2. GlobalScope - Use it in an Android application is a bad practice.
  3. MainScope - It is used generally use in an Activity.
  4. ViewModelScope - It is used for ViewModels. This scope cancels automatically.
  5. LifecycleScope - It is used for application components like Activities and Fragments. This scope cancels automatically.
Three are four Coroutine Builders
  1. Launch - This is used when you do not care about the results (Fire & Forget)
  2. Async - This is used when you expect result or output from your Coroutine.
  3. Produce - This is used in channels.
  4. Actor - This is used in conjuction with channels and pipeline concept.

Use of yield()

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.Main).launch {
task1()
}
CoroutineScope(Dispatchers.Main).launch {
task2()
}
}

private suspend fun task1()
{
Log.d(TAG , "Coroutines :: task1 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task1 : Starting Task 1")
yield()
Log.d(TAG , "Coroutines :: task1 : Ending Task 1")
}

private suspend fun task2()
{
Log.d(TAG , "Coroutines :: task2 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task2 : Starting Task 2")
yield()
Log.d(TAG , "Coroutines :: task2 : Ending Task 2")
}
}

Output

 2022-01-07 11:09:56.669 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : main
2022-01-07 11:09:56.669 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : Starting Task 1
2022-01-07 11:09:56.671 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : main
2022-01-07 11:09:56.671 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : Starting Task 2
2022-01-07 11:09:57.255 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : Ending Task 1
2022-01-07 11:09:57.256 5029-5029/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : Ending Task 2

Use of delay()

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.Main).launch {
task1()
}
CoroutineScope(Dispatchers.Main).launch {
task2()
}
}

private suspend fun task1()
{
Log.d(TAG , "Coroutines :: task1 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task1 : Starting Task 1")
delay(1000)
Log.d(TAG , "Coroutines :: task1 : Ending Task 1")
}

private suspend fun task2()
{
Log.d(TAG , "Coroutines :: task2 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task2 : Starting Task 2")
delay(1000)
Log.d(TAG , "Coroutines :: task2 : Ending Task 2")
}
}

Output

 2022-01-07 11:17:02.614 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : main
2022-01-07 11:17:02.614 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : Starting Task 1
2022-01-07 11:17:02.625 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : main
2022-01-07 11:17:02.625 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : Starting Task 2
2022-01-07 11:17:03.627 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task1 : Ending Task 1
2022-01-07 11:17:03.635 5275-5275/com.example.coroutines_android D/Coroutines: Coroutines :: task2 : Ending Task 2

Use of join() - Use the join method when you want to execute your code synchronously.

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
printFollowers()
}
}

private suspend fun printFollowers()
{
Log.d(TAG , "Coroutines :: task1 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task1 : Starting Task 1")
delay(1000)
Log.d(TAG , "Coroutines :: task1 : Ending Task 1")
var fbFollowers = 0
val job = CoroutineScope(Dispatchers.IO).launch {
fbFollowers = getFollowers()
}
job.join()
Log.d(TAG , "MainActivity :: printFollowers :: fbFollowers : $fbFollowers")
}

private suspend fun getFollowers() : Int
{
Log.d(TAG , "Coroutines :: task2 : ${Thread.currentThread().name}")
Log.d(TAG , "Coroutines :: task2 : Starting Task 2")
delay(1000)
Log.d(TAG , "Coroutines :: task2 : Ending Task 2")
delay(5000)
return 54
}
}

Output

2022-01-07 18:50:16.799 7642-7687/com.example.coroutines_android D/Coroutines: MainActivity :: printFollowers :: fbFollowers : 54

Use of async() and await()

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
printFollowers()
}
}

private suspend fun printFollowers()
{
val fb = CoroutineScope(Dispatchers.IO).async {
getFollowers()
}
val insta = CoroutineScope(Dispatchers.IO).async {
getInstaFollowers()
}
Log.d(TAG , "MainActivity :: printFollowers :: fbFollowers : ${fb.await()}")
Log.d(TAG , "MainActivity :: printFollowers :: instaFollowers : ${insta.await()}")
}

private suspend fun getFollowers() : Int
{
delay(1000)
return 54
}

private suspend fun getInstaFollowers() : Int
{
delay(1000)
return 113
}
}

Output

 2022-01-07 20:29:31.967 9043-9085/com.example.coroutines_android D/Coroutines: MainActivity :: printFollowers :: fbFollowers : 54
2022-01-07 20:29:31.969 9043-9085/com.example.coroutines_android D/Coroutines: MainActivity :: printFollowers :: instaFollowers : 113

Use of async and await with launch()

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
printFollowers()
}
}

private suspend fun printFollowers()
{
CoroutineScope(Dispatchers.IO).launch {
val fb = async { getFollowers() }
val insta = async { getInstaFollowers() }
Log.d(TAG , "MainActivity :: printFollowers :: fbFollowers : ${fb.await()}")
Log.d(TAG , "MainActivity :: printFollowers :: instaFollowers : ${insta.await()}")
}
}

private suspend fun getFollowers() : Int
{
delay(1000)
return 54
}

private suspend fun getInstaFollowers() : Int
{
delay(1000)
return 113
}
}

Output

 2022-01-08 00:31:09.355 9362-9408/com.example.coroutines_android D/Coroutines: MainActivity :: printFollowers :: fbFollowers : 54
2022-01-08 00:31:09.355 9362-9408/com.example.coroutines_android D/Coroutines: MainActivity :: printFollowers :: instaFollowers : 113

Parent-Child Coroutine Version 1

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
execute()
}
}

private suspend fun execute()
{
val parentJob = GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG , "MainActivity :: execute :: parentJob :: ${this.coroutineContext}")
val childJob = launch {
Log.d(TAG , "MainActivity :: execute :: childJob :: ${this.coroutineContext}")
}
}
}
}

Output

 2022-01-08 00:53:46.068 9614-9614/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: parentJob :: [StandaloneCoroutine{Active}@ff2c9f2, Dispatchers.Main]
2022-01-08 00:53:46.071 9614-9614/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: childJob :: [StandaloneCoroutine{Active}@713e843, Dispatchers.Main]

Parent-Child Coroutine Version 2

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
execute()
}
}

private suspend fun execute()
{
val parentJob = GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG , "MainActivity :: execute :: parentJob :: ${this.coroutineContext}")
val childJob = launch(Dispatchers.IO) {
Log.d(TAG , "MainActivity :: execute :: childJob :: ${this.coroutineContext}")
}
}
}
}

Output

 2022-01-08 01:17:20.915 9859-9859/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: parentJob :: [StandaloneCoroutine{Active}@ff2c9f2, Dispatchers.Main]
2022-01-08 01:17:20.917 9859-9903/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: childJob :: [StandaloneCoroutine{Active}@713e843, Dispatchers.IO]

Parent-Child Version 3

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
execute()
}
}

private suspend fun execute()
{
val parentJob = GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG , "MainActivity :: execute :: Parent Started")
val childJob = launch(Dispatchers.IO) {
Log.d(TAG , "MainActivity :: execute :: Child Started")
delay(5000)
Log.d(TAG , "MainActivity :: execute :: Child Ended")
}
delay(3000)
Log.d(TAG , "MainActivity :: execute :: Parent Ended")
}
parentJob.join()
Log.d(TAG , "MainActivity :: execute :: Parent Completed")
}
}

Output

 2022-01-08 01:34:04.679 10318-10318/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Started
2022-01-08 01:34:04.684 10318-10365/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Child Started
2022-01-08 01:34:07.689 10318-10318/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Ended
2022-01-08 01:34:09.690 10318-10365/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Child Ended
2022-01-08 01:34:09.692 10318-10365/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Completed

Parent Job Cancel

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
execute()
}
}

private suspend fun execute()
{
val parentJob = GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG , "MainActivity :: execute :: Parent Started")
val childJob = launch(Dispatchers.IO) {
Log.d(TAG , "MainActivity :: execute :: Child Started")
delay(5000)
Log.d(TAG , "MainActivity :: execute :: Child Ended")
}
delay(3000)
Log.d(TAG , "MainActivity :: execute :: Parent Ended")
}
delay(1000)
parentJob.cancel()
parentJob.join()
Log.d(TAG , "MainActivity :: execute :: Parent Completed")
}
}

Output

 2022-01-08 01:45:17.563 10520-10520/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Started
2022-01-08 01:45:17.564 10520-10565/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Child Started
2022-01-08 01:45:18.659 10520-10563/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Completed

Child Job Cancel

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
execute()
}
}

private suspend fun execute()
{
val parentJob = GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG , "MainActivity :: execute :: Parent Started")
val childJob = launch(Dispatchers.IO) {
Log.d(TAG , "MainActivity :: execute :: Child Started")
delay(5000)
Log.d(TAG , "MainActivity :: execute :: Child Ended")
}
delay(3000)
childJob.cancel()
Log.d(TAG , "MainActivity :: execute :: Parent Ended")
}
parentJob.join()
Log.d(TAG , "MainActivity :: execute :: Parent Completed")
}
}

Output

 2022-01-08 01:53:49.365 10816-10816/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Started
2022-01-08 01:53:49.374 10816-10902/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Child Started
2022-01-08 01:53:52.383 10816-10816/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Ended
2022-01-08 01:53:52.386 10816-10902/com.example.coroutines_android D/Coroutines: MainActivity :: execute :: Parent Completed

withContext - It behaves like a suspend function. Means it will block the function until it is executed.

 class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "Coroutines"
}

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
executeTask()
}
}

private suspend fun executeTask()
{
Log.d(TAG , "MainActivity :: executeTask :: Before")
withContext(Dispatchers.IO) {
delay(1000)
Log.d(TAG , "MainActivity :: executeTask :: Inside")
}
Log.d(TAG , "MainActivity :: executeTask :: After")
}

}

Output

 2022-01-08 03:03:59.139 11757-11800/com.example.coroutines_android D/Coroutines: MainActivity :: executeTask :: Before
2022-01-08 03:04:00.173 11757-11800/com.example.coroutines_android D/Coroutines: MainActivity :: executeTask :: Inside
2022-01-08 03:04:00.175 11757-11800/com.example.coroutines_android D/Coroutines: MainActivity :: executeTask :: After

Do's and Dont's

  • Do not use GlobalScope or runblocking in your Android Application. They both will make your code synchronous rather than asynchronous.
  • If you are using Activity then use can use lifeCycleScope to launch/async/produce/actor, a corutine directly in a function. 
  • If you are using Fragment then use viewLifeCycleOwner to launch/async/produce/actor, a coroutine with the help of lifeCycleScope in a function.
 viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
getAEPSResponse()
}
}
  • If you are using ViewModel then you can directly use viewModelScope to launch/ async/produce/actor, a coroutine directly in a function.
  • If you get the error "Only the original thread that created a view hierarchy can touch its views." try the following code
 activity?.runOnUiThread(Runnable {
edtName.setText(this.name)
edtDOB.setText(this.dob)
edtAddress.setText(this.address)
edtPinCode.setText(this.pin)
})
  • Do not forget to specify the dispatcher of a child coroutine. If you do not specify it then it will take the dispatcher of parent coroutine.
  • When you are using withContext, you have to specify a dispatcher.

Reference




Comments

Popular posts from this blog

SSLSocketFactory in Android

Architecture Components in Android

DataBinding in Android