Coroutines on Android

Hamza Oban
Orion Innovation techClub
4 min readMar 21, 2023
Coroutines

What are the coroutines?

A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously.

On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive.

Implementation

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Features

Features that will help us understand are:

Lightweight: You can run many coroutines on a single thread due to support for suspension, which doesn’t block the thread where the coroutine is running.

 runBlocking {
repeat(10000){//lots of coroutines
launch {
delay(1000)
println("Android")
}
}
}

Fewer memory leaks: Use structured concurrency to run operations within a scope.

Built-in cancellation support: Cancellation is propagated automatically through the running coroutine hierarchy.

suspend fun main() {

val job = CoroutineScope(Dispatchers.IO).launch {
repeat(1000) { i ->
println("job: $i ...")
delay(500L)
}
}
delay(1300L) // delay a bit
job.cancel() // cancels the job
job.join() // waits for job's completion
println("End")
}

Output :

job: 0 …
job: 1 …
job: 2 …
End

When ” job.cancel” is called, we do not see any output from the other coroutine as it is canceled. To use coroutine scope we should use suspend fun.

Jetpack integration: Many Jetpack libraries are coroutine compatible and include extensions that provide full coroutine support.

Android KTX

Some libraries even have their coroutines.

For examples: ViewModelScope, LifecycleScope, LiveData

GlobalScope: GlobalScope is alive as long as your app is alive if you do some counting for instance in this scope and rotate your device it will continue the task/process. We don’t need to write it in Suspend fun.

class MainActivity : AppCompatActivity() {
private val TAG = "GLOBAL"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
repeat(100000){
launch {
Log.d(TAG,"GlobalScope")
}
}
}
}
}

Launch: is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently

runBLocking: It stops the execution of the next code block, it blocks.

fun main() {
runBlocking{
this.launch{
repeat(10){
println("Android + $it")
}

}
}
println("End")
}

Output:

Android + 0
Android + 1
Android + 2
Android + 3
Android + 4
Android + 5
Android + 6
Android + 7
Android + 8
Android + 9
End

CoroutineContext

CoroutineContext is an indexed set of elements that define the behavior of a Coroutine.

Items related to CoroutineContext are:

  1. CoroutineDispatcher: Dispatches the job to the appropriate thread.

Dispatchers.IO: Networking or reading and writing from files (any input and output)

Dispatchers.Default: For CPU-intensive tasks such as sorting large lists and performing complex calculations.

Dispatchers.Main: For everything else, new UI or non-blocking code.

2. CoroutineExceptionHandler: It’s better to use CoroutineExceptionHandler inside CoroutineScope instead of try-catch.

lifecycleScope.launch {

try {
launch {
throw Exception("error")
}
} catch (e: Exception) {
print(e.stackTrace)
}
}

This won’t work. Exceptions propagate in coroutines and we need CorutineExceptionHandler

        val handler = CoroutineExceptionHandler {context, throwable ->
println("exception: " + throwable)
}

/* this will cancel the other coroutine without executing it.
so if we are doing many launches, we can use SupervisorScope.
*/

//an error will cancel all initializations within the scope of this coroutine
lifecycleScope.launch(handler) {
launch {
throw Exception("error")
}
launch {
delay(500L)
println("this is executed")
}
}


/* All initializations within the scope of the other
coroutine will continue even if there is an error */
lifecycleScope.launch(handler) {
supervisorScope {
launch {
throw Exception("error")
}
launch {
delay(500L)
println("this is executed")
}
}
}

//we can use the handler in other cases as well such as
CoroutineScope(Dispatchers.Main + handler).launch {
launch {
throw Exception("error in a coroutine scope")
}
}

Applying Retrofit with Coroutines

First, we need a model.

You need to add the properties from your API to your model.

data class CryptoModel(
val currency: String,
val price: String
)

Launch the coroutine and make the Retrofit network call in the background. In CryptoAPI, make getData() a suspend function. So that you can call this method from within a coroutine.

interface CryptoAPI {
@GET("Your-Api-Key-Or-Json-Url")
suspend fun getData(): Response<List<CryptoModel>>
}

We created the Retrofit object in Activity. In the RecyclerView we showed the data asynchronously with the coroutine.

private fun loadData() {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CryptoAPI::class.java)

job = CoroutineScope(Dispatchers.IO).launch {
val response = retrofit.getData()
withContext(Dispatchers.Main){
if (response.isSuccessful){
response.body()?.let {
cryptoModels = ArrayList(it)
cryptoModels.let { list ->
recyclerViewAdapter = list?.let { it1 -> RecyclerViewAdapter(it1,this@MainActivity) }
}
}
}
}
}
}

When an activity is destroyed, we end the job with the job object we created.

override fun onDestroy() {
super.onDestroy()
job?.cancel()
}

Thanks for reading and if you like the article, remember to clap.

--

--