One practical example of using coroutines for a production application is in loading data from separate data sources. With other approaches or frameworks, this might require multiple callbacks or the use of something along the lines of reactive streams. These approaches can be difficult to read and follow. Coroutines can make this type of use case much simpler:
- First, let's define a new main() function that simulates the initialization of a screen in our application:
fun main() = runBlocking {
println("show loading....")
launch {
println("loaded data = ${loadData()}")
}
println("called loadData()")
}
- We are launching a new coroutine that calls loadData() and then prints out its output. Because this is done within a coroutine, the execution of loadData() does not delay the remainder of our initialization:
show loading....
called loadData()
loaded data = 9
- Now, let's see how loadData() is implemented:
suspend fun loadData() : Int {
return loadFromSource1() + loadFromSource2()
}
- The loadData() function is very straightforward. It is simply returning the added values of loadFromSource1() and loadFromSource2():
suspend fun loadFromSource1() : Int {
delay(1000)
return 3
}
suspend fun loadFromSource2() : Int {
delay(4000)
return 6
}
In this case, loadFromSource1() and loadFromSource2() delay for some period of time and return an integer. However, this could easily be a blocking network request or database operation. In any of these cases, the calling code does not know how long the function will take to return, which is why they've been implemented as suspending functions.
Because both of our loading functions are suspending functions, loadData() must also be a suspending function. By leveraging coroutines for this type of code, the result is sequential, efficient, and easy-to-follow asynchronous code.