In this article, we will explore the concept of coroutines as a means of handling background tasks in our applications. Coroutines are the go-to solution when it comes to handling Immediate impersistent tasks. A lot of Android libraries come with coroutines support out of the box making it easier to perform those libraries’ functionalities incorporated with coroutines.
Coroutines are a type of function that allows you to pause the execution of a function, perform another task, and then return to the paused function to continue execution from where it left off. This makes them ideal for handling asynchronous tasks such as network calls, database operations, and other long-running tasks.
Advantages of Coroutines
- Simplicity: Coroutines are easy to use and implement compared to other concurrency models such as threads and reactiveX.
- Efficiency: Coroutines are lightweight and have less overhead compared to threads, making them more efficient when handling a large number of tasks.
- Readability: Coroutines use a sequential style of coding, making them more readable and easier to understand than callback-based coding.
let’s try to break down each of the concepts in the Coroutines.
Coroutine Scopes
Coroutine scopes are used to manage the lifecycle of coroutines. They determine when a coroutine should start and when it should be canceled. There are built-in coroutine scopes in KTX extensions. let’s take a look at each one of these.
Global Scope
Coroutine global scope is used when a scope is not bound to any Android component. It is an application-wide scope that lives as long as your application is running. It is typically used for long-running tasks that are not tied to a specific UI component. GlobalScope should be done with caution since it is not bound to any Android component It will not be cancelled automatically. If you have defined coroutine scope it’s your responsibility to close it.
fun main() {
GlobalScope.launch {
delay(1000)
println("Coroutine executed in GlobalScope")
}
Thread.sleep(2000) // Wait for the coroutine to finish
}
Lifecycle Scope
Coroutine lifecycle scope is used when there is a task that needs to execute tide to a lifecycle of any android component that has a lifecycle. When the associated component is destroyed or its scope is canceled, all coroutines launched in that scope are automatically canceled as well. androidx.lifecycle:lifecycle-runtime-ktx dependency is required to use lifecycle scope.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
delay(1000)
println("Coroutine executed in lifecycleScope")
}
}
}
ViewModel Scope
viewModelScope
works the same as the lifecycle scope. viewModelScope
can be only used inside a ViewModel
class which is designed to get destroyed with it’s ViewModel
. androidx.lifecycle:lifecycle-runtime-ktx dependency is required to use this scope.
class MyViewModel : ViewModel() {
fun performTask() {
viewModelScope.launch {
// Simulate a long-running task
delay(3000)
println("Coroutine executed in viewModelScope")
}
}
}
Main Scope
The main scope is specifically designed to launch coroutines on the main thread. This makes it suitable for updating UI components and performing other UI-related tasks. Since Main scope doesn’t care about the lifecycle of the UI component, when using this scope it’s your responsibility to manage it. org.jetbrains.kotlinx:kotlinx-coroutines-android dependency is required to use this scope.
class MainActivity : AppCompatActivity() {
private val mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainScope.launch {
delay(1000)
println("Coroutine executed in MainScope")
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel() // Cancel the scope and all associated coroutines
}
}
Apart from the above-mentioned scopes, there is an option to define our own Coroutine scope with a custom job.
Coroutine Context
Coroutine context is a set of various elements that can be provided when building a coroutine which helps us to control certain aspects of the created coroutine.
Dispatcher
The dispatcher is responsible for determining which thread or thread pool the coroutine should be executed on.
- Dispatchers.Main: Dispatches coroutines to the main (UI) thread. It is used for UI-related operations such as updating UI components.
- Dispatchers.Main: Dispatches coroutines to the main (UI) thread. It is used for UI-related operations such as updating UI components.
- Dispatchers.Default: Used for CPU-bound operations that are not particularly sensitive to the execution thread, such as heavy computations or sorting algorithms. It uses a shared pool of threads optimized for CPU-intensive tasks.
// Running a coroutine on the I/O thread
GlobalScope.launch(Dispatchers.IO) {
// Perform I/O operations here
}
Job
Jobs represent the state of the coroutine. They allow us to manage, cancel, and await the completion of a coroutine.
val job = GlobalScope.launch {
// Coroutine code here
}
// To cancel the coroutine
job.cancel()
Coroutine ExceptionHandler
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
// Handle the exception here
}
GlobalScope.launch(exceptionHandler) {
// Coroutine code here
}
Coroutine Builders
Coroutine builders are functions used to launch coroutines. They provide different ways to start and manage coroutines, depending on the use case and requirements. There are three main coroutine builders
Launch
launch
is used to start a new coroutine asynchronously, which means it will not block the calling thread. It returns a Job
that represents the coroutine being executed. It is commonly used for fire-and-forget tasks, where you don’t need the result of the coroutine’s computation.
fun main() {
GlobalScope.launch {
println("Coroutine is starting...")
delay(1000)
println("Coroutine completed!")
}
// Main thread will not be blocked here
println("Main thread continues...")
Thread.sleep(2000)
}
RunBlocking
runBlocking
is used to start a new coroutine, but it blocks the current thread until the coroutine is completed. It’s typically used in testing or in cases where you need to wait for the coroutine’s result before proceeding. It is not recommended to use runBlocking
in the context of UI threads or long-running tasks, as it will block the thread. The example in the next coroutine builder will cover the runBlocking
as well.
Async
async
is used to start a new coroutine asynchronously like launch, but it also returns a Deferred object representing the result of the coroutine computation. Deferred is a lightweight non-blocking future. It is useful when you need to perform a computation concurrently and later retrieve its result.
suspend fun doComputation(): Int {
delay(1000)
return 42
}
fun main() {
val deferredResult: Deferred<Int> = GlobalScope.async {
doComputation()
}
// Do other work here...
// Wait for the result and retrieve the value using await()
runBlocking {
val result = deferredResult.await()
println("Result: $result")
}
}
Suspending Functions
Suspending functions are used to perform asynchronous operations, such as making network calls, reading/writing to files, or performing long-running computations. They allow you to write non-blocking code that can be executed concurrently. suspending functions are always executed within a coroutine scope. You can call suspending functions from other coroutines or other suspending functions. Suspending functions are declared using the suspend
keyword.
suspend fun mySuspendingFunction() {
// Coroutine code here
}
The beauty of coroutine is for most of the common user cases you have built-in support for using coroutine even without the above knowledge. The most common tasks that we generally do in most of the Android applications are implementing network calls and SQLite database implementation. Both of above mentioned tasks can not be performed in the main thread so we need a threading solution. For implementing network calls using Retrofit we have out-of-the-box support for coroutine. For implementing SQLite database operations, Room library has built-in support to work with coroutine.
Network call implementation with coroutine
For this example, I’m using this repository which I have used for my previous Android MVVM architecture with clean error handling series. let’s take a look at how a network call can be implemented using coroutine as the threading solution.
To start the implementation we need to import the following dependencies to the Android project.
//coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//Moshi
def moshi_version = "1.9.2"
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.retrofit2:converter-moshi:2.4.0"
Once the above dependencies are added, we can begin implementing network calls with coroutines. Let’s examine the ApiService
class, which defines the endpoints that the application calls. In this interface, we can define these interfaces in the same way we are customed to. The only difference is that these functions should be defined as suspend
so that they can be called from a coroutine function or from a coroutine scope.
interface ApiService {
@GET("items/plantDetails/{plantId}")
suspend fun getPlantDetails(@Path("plantId") plantId: Int): PlantDetailResponse
}
This application is built with MVVM architecture. Because of that, all the network call operations are handled in the repository layer. The function is in the repository also defined as a suspend function for same above-mentioned reasons. Let’s take a look at that function in PlantDetailsRepository
class.
class PlantDetailsRepository @Inject constructor(private val apiService: ApiService)
: BaseRepository() {
val plantDetailLiveData = MutableLiveData<PlantDetails>()
suspend fun getPlantDetails(plantId: Int) {
try {
val response = apiService.getPlantDetails(plantId)
if (response.status) {
val (id, name, image, price, sizes, planters, description) = response.data
plantDetailLiveData.value =
PlantDetails(id, name, image, price, sizes, planters, description)
} else {
responseError(response.message)
}
} catch (exception: Exception) {
when (exception) {
is HttpException -> connectionError(exception.message())
is ConnectivityInterceptor.NoConnectivityException -> noConnectivityError()
is SocketTimeoutException -> timeoutConnectionError()
is IOException -> processingError()
else -> processingError()
}
}
}
}
Finally, in the PlantDetailsViewModel class, I have used the viewModelScope
to call the getPlantDetails(plantId: Int)
function defined above in the repository. Since it is an suspend
it’s possible to call from a coroutine scope.
private fun getPlantDetail(plantId: Int){
viewModelScope.launch {
isProcessing.value = ProcessingStatus.PROCESSING
repository.getPlantDetails(plantId)
}
}
You may be wondering now why I haven’t had to define the coroutine context as the Dispatchers.IO
when I’m performing the network call. This is due to having built-in support in the Retrofit library for coroutine. when you use the suspend
key work on the functions in ApiService
interface, the appropriate thread to execute the network call is set and handled by the library.
SQLite database implementation with coroutine.
For this, I’m using an example that I will use in future article to explain WorkManager.
In this example, I have used a form to insert a few data into SQLite database called “deliveryHistory”. I’m going to focus on database operation in this example. First, let’s take a look at the dependencies that needed to be included.
//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
//Room
def room_version = "2.5.0"
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
In Room database implementation we have an interface that defines all the functions that are needed to execute in a particular table. We call this “Dao” interface. just like in Retrofit, all you have to do is define these functions as suspend
functions.
@Dao
interface DeliveryHistoryDao {
@Insert
suspend fun insert(history: DeliveryHistory)
}
In this project, I’m not using MVVM architecture and jetpack composer for UI. I’m performing the database function directly from the composer screen.
@Composable
@Preview
fun PeriodicWorkManagerExampleScreen(navController: NavController = rememberNavController()) {
val applicationContext = context.applicationContext as ThisApplication
val coroutineScope = rememberCoroutineScope()
fun onClickSave() {
validateAddress(addressValue)
validateNIC(nicFieldValue)
validateDropOffType(dropOffTypeFieldValue)
if (addressFiledError.isEmpty() && nicFieldError.isEmpty() && dropOffTypeFieldError.isEmpty()) {
coroutineScope.launch {
val deliveryHistory = DeliveryHistory(
address = addressValue,
nic = nicFieldValue,
type = dropOffTypeFieldValue
)
applicationContext.database.deliveryHistoryDao().insert(deliveryHistory)
Toast.makeText(context, "Recode added", Toast.LENGTH_SHORT).show()
addressValue = ""
nicFieldValue = ""
dropOffTypeFieldValue = ""
resetDropDown = true
delay(100)
resetDropDown = false
focusManager.clearFocus()
}
} else {
Toast.makeText(context, "Please complete the from", Toast.LENGTH_SHORT).show()
}
}
}
In a composer screen, we can get the coroutine scope by using rememberCoroutineScope()
. The data from the form is inserted into the “DeliveryHistory” table in this application in onClickSave()
function that triggers when the user clicks on the save button. Since SQLite also supports coroutine out of the box we do not need to provide coroutine context Dispatchers.IO
just like in the network call implementation example. The process of selecting the appropriate thread to handle the task is configured by the library internally.
Here we covered all the major aspects of coroutines in Kotlin and how they can be used to handle background tasks in our applications. We discussed coroutine scopes, coroutine context, coroutine builders, and suspending functions. We also saw how to implement network calls and SQLite database operations using coroutines. Coroutines are a powerful tool in the Kotlin language that can help make our code more efficient, readable, and easier to maintain.