Kotlin 1.3 introduced the concept of co-routes for asynchronous behaviour. In the Java world, threads have been pretty common however slightly hard to follow model. Promises solved this problem to some extent but continued to be painful to write and read code. Kotlin however borrowed from other languages some good patterns and created this feature of coroutines. In this part we will discuss this in greater depth.
Why co-routines
In Android, there is a concept of UI thread. A thread that is responsible for building UI of your App and responding to your touches and other interactions. The UI thread should be used as little as possible. If there is any long running operation that blocks UI thread there is a danger of your app becoming unresponsive and hence could lead to ANR (Application Not Responding) errors.
In Android, hence it is recommended and enforced that IO operations be done on a separate thread distinct from UI thread. This leads to the situation where you have no option but to use threads of some sort.
Kotlin's co-routines provide you a very concise syntax to create operations that run on a separate thread with separate context. Coroutines are very lightweight, have better memory management and have inbuilt cancellation support which solves a lot of common problems Android developers faced with Java's support for threads.
Using co-routines
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)
}
}
}
Above simple code is take from official website of android documentation. The most common requirement for asynchronous operations is network request. Network requests are IO operations that can take a very long time to execute and it bad user experience to freeze the UI during that time.
In above code, you see a special variable viewModelScope. Each coroutine is executed in a "scope". ViewModel has its own scope but you can define your own scopes as well. Each coroutine MUST run in a scope. A scope controls related coroutines. A scope manages lifecycle of all the coroutines that are running in it. For example when the scope itself is cancelled it can cancel all the coroutines as well.
The next important bit is the method launch. This takes one argument which is the dispatcher to be used to run the coroutine. For example in above code we use IO dispatcher which tells the scope that this is a coroutine meant to execute IO operations. Other dispatches are Default, Main and Unconfined. Which are to be used for CPU intensive operations, UI related interactions and for tasks that require a dedicated thread respectively.
Suspend functions
Those who have worked with Dart or NodeJs would know the "async" modifier of methods. When you have a function defined as "async", wherever you invoke it, you have to await the result. Do not worry if you don't know what I am talking about.
Imagine you are writing a Kotlin method for login. As the author you know that this is a method that is doing IO heavy operation. But other developers who are going to invoke your method might not know this. To fix this you can simply add a modifier to your method called "suspend" which forces other developers to always make sure this method is run in a coroutine. Also as the author of the method can actually decide what dispatcher to use.
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
}
}
}
Handling Exceptions
The hardest part of async code is handling exceptions. Coroutines recommend you use a Exception to catch any possible exception and handle it appropriately instead of bubbling it out.
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
}
}
}
}
Sharing mutable state in co-routines
How do different coroutines share state ? It is not possible to simply share variables in coroutines because the changes to that data structure are not always atomic. You must use thread safe data structures when dealing with coroutines.
val counterContext = newSingleThreadContext("CounterContext") var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// confine each increment to a single-threaded context
withContext(counterContext) { counter++ }
}
}
println("Counter = $counter") }
Conclusion
Coroutines are must know concept for Android developers using Kotlin for app development.