A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3
On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive.
Features
- 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. Suspending saves memory over blocking while supporting many concurrent operations.
- 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.
- Jetpack integration: Many Jetpack libraries include extensions that provide full coroutines support. Some libraries also provide their own coroutine scope that you can use for structured concurrency.
ViewModel
includes a set of KTX extensions that work directly with coroutines. These extension arelifecycle-viewmodel-ktx
library
To use coroutines in your Android project, add the following dependency to your app’s build.gradle
file:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
Executing in a background thread
Making a network request on the main thread causes it to wait, or block, until it receives a response. Since the thread is blocked, the OS isn’t able to call onDraw()
, which causes your app to freeze and potentially leads to an Application Not Responding (ANR) dialog. For a better user experience, run this operation on a background thread.
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
LoginRepository class:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
Let’s dissect the coroutines code in the login
function in LoginViewModel:
viewModelScope
is a predefinedCoroutineScope
that is included with theViewModel
KTX extensions. Note that all coroutines must run in a scope. ACoroutineScope
manages one or more related coroutines.launch
is a function that creates a coroutine and dispatches the execution of its function body to the corresponding dispatcher.Dispatchers.IO
indicates that this coroutine should be executed on a thread reserved for I/O operations.
Since this coroutine is started with viewModelScope
, it is executed in the scope of the ViewModel
. If the ViewModel
is destroyed because the user is navigating away from the screen, viewModelScope
is automatically cancelled, and all running coroutines are canceled as well.
One issue with the previous example is that anything calling makeLoginRequest
needs to remember to explicitly move the execution off the main thread.
Use coroutines for main-safety
We consider a function main-safe when it doesn’t block UI updates on the main thread. The makeLoginRequest
function is not main-safe, as calling makeLoginRequest
from the main thread does block the UI. Use the withContext()
function from the coroutines library to move the execution of a coroutine to a different thread:
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
withContext(Dispatchers.IO)
moves the execution of the coroutine to an I/O thread, making our calling function main-safe and enabling the UI to update as needed.
makeLoginRequest
is also marked with the suspend
keyword. This keyword is Kotlin’s way to enforce a function to be called from within a coroutine.
In the following example, the coroutine is created in the LoginViewModel
. As makeLoginRequest
moves the execution off the main thread, the coroutine in the login
function can be now executed in the main thread:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
Note that the coroutine is still needed here, since makeLoginRequest
is a suspend
function, and all suspend
functions must be executed in a coroutine.
This code differs from the previous login
example in a couple of ways:
launch
doesn’t take aDispatchers.IO
parameter. When you don’t pass aDispatcher
tolaunch
, any coroutines launched fromviewModelScope
run in the main thread.- The result of the network request is now handled to display the success or failure UI.
The login function now executes as follows:
- The app calls the
login()
function from theView
layer on the main thread. launch
creates a new coroutine to make the network request on the main thread, and the coroutine begins execution.- Within the coroutine, the call to
loginRepository.makeLoginRequest()
now suspends further execution of the coroutine until thewithContext
block inmakeLoginRequest()
finishes running. - Once the
withContext
block finishes, the coroutine inlogin()
resumes execution on the main thread with the result of the network request.
There are 3 scopes in Kotlin coroutines:
- Global Scope
- LifeCycle Scope
- ViewModel Scope
Global Scope
When Coroutines are launched within the global scope, they live long as the application does. If the coroutines finish it’s a job, it will be destroyed and will not keep alive until the application dies, but if the coroutines has some work or instruction left to do, and suddenly we end the application, then the coroutines will also die, as the maximum lifetime of the coroutine is equal to the lifetime of the application.
GlobalScope.launch {
}
LifeCycle Scope
When Coroutines are launched within the lifecycle scope, they live long as the activity does. All the coroutines launched within the activity also dies when the activity dies.
lifecycleScope.launch {
}
ViewModel Scope
When Coroutines are launched within the viewModel scope, they live long as the viewModel does.
viewModelScope.launch {
}
Kotlin Coroutines vs Threads
- Concurrency Model:
- Threads: Traditional threads are managed by the operating system. Each thread has its own stack and is scheduled by the OS kernel. This can lead to potential overhead due to context switching.
- Coroutines: Kotlin Coroutines are a higher-level concurrency abstraction. They are lightweight and can be implemented without relying on OS threads directly. They allow for cooperative multitasking, where a coroutine voluntarily suspends its execution, allowing other coroutines to run.
- Resource Usage:
- Threads: Creating and managing threads can be resource-intensive. Threads have a higher memory overhead due to their individual stacks.
- Coroutines: Coroutines are lighter on resources. They can be Combined onto a smaller number of threads, making more efficient use of system resources.
- Synchronization and Communication:
- Threads: Synchronization between threads often requires the use of locks, which can lead to issues like deadlock and increased complexity.
- Coroutines: Coroutines use structured concurrency, and synchronization is typically achieved using language constructs like
suspend
andawait
. This can lead to more readable and maintainable code.
- Error Handling:
- Threads: Error handling in multithreaded code can be challenging. Unhandled exceptions in one thread might affect the entire application.
- Coroutines: Coroutines provide a more structured and easy-to-handle approach to error management. Exceptions are propagated through the coroutine hierarchy, and there are constructs like
try/catch
for handling errors.
- Cancellation:
- Threads: Cancelling threads can be challenging and may lead to resource leaks.
- Coroutines: Coroutines support structured cancellation. When a coroutine is cancelled, it can clean up resources and handle the cancellation gracefully.
- Compatibility:
- Threads: Traditional threads are suitable for all types of Java applications and can be used in Kotlin as well.
- Coroutines: Coroutines are specific to Kotlin and may not be as interoperable with Java code as threads.
References:
https://developer.android.com/kotlin/coroutines
https://www.geeksforgeeks.org/scopes-in-kotlin-coroutines/