Android Kotlin Coroutines: Basic & Usage
This Story is from the Kotlin- Series.For begginer please follow my previous link for coroutines Stepup.This article is a brief summary of official Kotlin doc and a few other resources that I have gone through over time during my learning.
What are Coroutines?
A coroutine is an instance of suspendable computation, conceptually similar to a thread, in the sense that it takes a block of code to run and has a similar life-cycle, it is created and started, but it is not bound to any particular thread.On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive. Over 50% of professional developers who use coroutines have reported seeing increased productivity.
Coroutines are lightweight threads and they are so cheap to create which open the doors to asynchronous programming for developers. This means we can call more than one functions asynchronously and get the result in a shorter duration in very effective way. Now imagine how faster our validations would be as we can call them asynchronously. All those things (API calls, Database access…) which need a background thread to process are just a few lines away than creating separate worker threads for it and then managing it. And making coroutines life-cycle aware is so simple.
suspend fun validateEntries(str: String): Boolean = coroutineScope {
val deferred1 = async { printDelayed(str) }
val deferred2 = async { printDelayed(str) }
deferred1.await() && deferred2.await()
}
CoroutineBuilder
A function that takes some suspending lambda (lambda marked with suspend
modifier) as an argument, creates a coroutine, and, optionally, gives access to its result in some form.
Coroutine builders provided by kotlinx.coroutines:
# launch {}: Launch a coroutine that does not have any result, it returns Job
.
# async{}: Returns a single value result using Deffered
- a light-weight non-blocking future that represents a promise to provide a result later. So, we can use .await()
on a deferred value to get its eventual result, but Deferred
is also a Job
, so we can cancel it if needed.
# runBlocking{}: Blocks current thread until the execution of coroutine.
# delay:
is a special suspending function that does not block a thread but suspends coroutine.
Examples overview
The examples in this topic make a network request and return the result to the main thread, where the app can then display the result to the user.
ViewModel
includes a set of KTX extensions that work directly with coroutines. These extension are lifecycle-viewmodel-ktx
library and are used in this guide.
First, let’s take a look at our Repository
class and see how it's making the network request:
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"))
}
}
makeLoginRequest
is synchronous and blocks the calling thread. To model the response of the network request, we have our own Result
class.
The ViewModel
triggers the network request when the user clicks, for example, on a button:
class LoginViewModel(private val loginRepository: LoginRepository): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
With the previous code, LoginViewModel
is blocking the UI thread when making the network request. The simplest solution to move the execution off the main thread is to create a new coroutine and execute the network request on an I/O 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)
}
}
}
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.
Let's see how we can modify the Repository
to solve this problem for us.
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.
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.
Handling exceptions
To handle exceptions that the Repository
layer can throw, use Kotlin's built-in support for exceptions. In the following example, we use a try-catch
block:
class LoginViewModel(private val loginRepository: LoginRepository
): ViewModel() {
fun makeLoginRequest(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
In this example, any unexpected exception thrown by the makeLoginRequest()
call is handled as an error in the UI.
If you liked the article, clap clap clap 👏👏👏 as many times as you can.
LIKED SO MUCH! Medium allows up to 50 claps.