Using Kotlin coroutines it’s really easy to execute a task in a background thread and update the UI based on the result. Just enter the coroutine world using the launch method and then change thread using withContext. It’s even simpler if the task is an http call (thanks to coroutines support in retrofit) or a database query (thanks to Room). The final code is the same we’d use to execute synchronous code. But coroutines are more than just a tool to switch thread, we can use them to execute tasks in parallel. The code is still really easy to read but sometimes it can be difficult to write: we need to pay attention to many aspects (like nested scopes, exceptions and dispatchers). In this talk we’ll see how to leverage the coroutines library to manage parallelism, from the basic concepts to some advanced example.
18. interface StackOverflowService {
@GET("/users")
suspend fun fetchTopUsers(): List<User>
@GET("/users/{userId}/badges")
suspend fun fetchBadges(
@Path("userId") userId: Int
): List<Badge>
@GET("/users/{userId}/top-tags")
suspend fun fetchTags(
@Path("userId") userId: Int
): List<Tag>
}
19. class Storage {
fun load(): List<User>? {3
val content = FileInputStream(getCacheFile())
.bufferedReader()
.use { it.readText() }
return parseData(content)
}1
}2
20. class Storage {
fun load(): List<User>? = withContext(Dispatchers.IO) {3
val content = FileInputStream(getCacheFile())
.bufferedReader()
.use { it.readText() }
return parseData(content)
}1
}2
21. class Storage {
suspend fun load(): List<User>? = withContext(Dispatchers.IO) {
val content = FileInputStream(getCacheFile())
.bufferedReader()
.use { it.readText() }
return parseData(content)
}1
}2
22. viewModelScope.launch {
!//!!...
val dataFromCache = storage.load()
val data = if (dataFromCache "!= null) {
dataFromCache
} else {
val fetchedData = api.fetchTopUsers()[0]
storage.save(fetchedData)
fetchedData
}3
updateUi(data)
!//!!...
}1
23. viewModelScope.launch {
!//!!...
val dataFromCache = storage.load()
val data = if (dataFromCache "!= null) {
dataFromCache
} else {
val fetchedData = api.fetchTopUsers()[0]
println("where am I executed?")
storage.save(fetchedData)
fetchedData
}3
updateUi(data)
!//!!...
}1
main
main
io
main
main
main
io
main
io
main
main
main
main
main
24. viewModelScope.launch {
!//!!...
val data = withContext(Dispatchers.IO) {
val dataFromCache = storage.load()
if (dataFromCache "!= null) {
dataFromCache
} else {
val fetchedData = api.fetchTopUsers()[0]
println("where am I executed?")
storage.save(fetchedData)
fetchedData
}3
}
updateUi(data)
!//!!...
}1
main
main
io
io
io
io
io
io
io
io
io
io
io
main
main
main
27. suspend fun topUser(): UserStats {
val user = api.fetchTopUsers()[0]
val badges = api.fetchBadges(user.id)
val tags = api.fetchTags(user.id)
return UserStats(
user,
badges,
tags
)2
}1
28. suspend fun topUser(): UserStats {
val user = api.fetchTopUsers()[0]
val badges = api.fetchBadges(user.id)
val tags = api.fetchTags(user.id)
return UserStats(
user,
badges,
tags
)2
}1
fun topUser(): Single<UserStats> =
api.fetchTopUsers()
.map { users "-> users[0] }
.flatMap { user "->
api.fetchBadges(user.id)
.flatMap { badges "->
api.fetchTags(user.id).map { tags "->
UserStats(user, badges, tags)
}
}
}
RxJavaCoroutines
32. suspend fun topUser(): UserStats {
val user = api.fetchTopUsers()[0]
val badges = api.fetchBadges(user.id)
val tags = api.fetchTags(user.id)
return UserStats(
user,
badges,
tags
)2
}1
33. suspend fun topUser(): UserStats {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
}
return UserStats(
user,
badges,
tags
)2
}1
34. suspend fun topUser(): UserStats {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
}
return UserStats(
user,
badges,
tags
)2
}1
39. !!/**
* !!...
* The resulting coroutine has a key difference compared with similar
* primitives in other languages and frameworks:
* it cancels the parent job (or outer scope) on failure
* to enforce structured concurrency paradigm.
* !!...
!*/
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> T
): Deferred<T>
40. !!/**
* !!...
* The resulting coroutine has a key difference compared with similar
* primitives in other languages and frameworks:
* it cancels the parent job (or outer scope) on failure
* to enforce structured concurrency paradigm.
* !!...
!*/
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> T
): Deferred<T>
41. !!/**
* !!...
* The resulting coroutine has a key difference compared with similar
* primitives in other languages and frameworks:
* it cancels the parent job (or outer scope) on failure
* to enforce structured concurrency paradigm.
* !!...
!*/
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> T
): Deferred<T>
42. viewModelScope.launch {
try {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
}
updateUi(UserStats(user, badges, tags))
} catch (e: Exception) {
showErrorMessage()
}1
}2
55. viewModelScope.launch {
try {
val user = api.fetchTopUsers()[0]
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
updateUi(UserStats(user, badgesDeferred.await(), tagsDeferred.await()))
} catch (e: Exception) {
showErrorMessage()
}1
}2
56. viewModelScope.launch {
val user = api.fetchTopUsers()[0]
val badgesDeferred = async {
runCatching {
api.fetchBadges(user.id)
}
}
val tagsDeferred = async {
runCatching {
api.fetchTags(user.id)
}
}
val badges = badgesDeferred.await()
val tags = tagsDeferred.await()
if (badges.isFailure "|| tags.isFailure) {
showErrorMessage()
}1
}2
In case of an error
the other is not cancelled
57. class MyClass(private val api: StackOverflowService) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = SupervisorJob() + Dispatchers.Main
suspend fun loadData(): UserStats {
val user = api.fetchTopUsers()[0]
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
val badges = badgesDeferred.await()
val tags = tagsDeferred.await()
return UserStats(user, badges, tags)
}1
}2
58. class MyClass(private val api: StackOverflowService) {
private val scope = MainScope()
suspend fun loadData(): UserStats {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
}3
return UserStats(user, badges, tags)
}1
}2
59. suspend fun loadData(): UserStats {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
}3
return UserStats(user, badges, tags)
}1
60. suspend fun loadData(): UserStats {
val user = api.fetchTopUsers()[0]
val (badges, tags) = coroutineScope {
try {
val badgesDeferred = async { api.fetchBadges(user.id) }
val tagsDeferred = async { api.fetchTags(user.id) }
badgesDeferred.await() to tagsDeferred.await()
} catch (e: Exception) {
emptyList<Badge>() to emptyList<Tag>()
}4
}3
return UserStats(user, badges, tags)
}1
70. !!/**
* Launches a new coroutine without blocking the current thread and
* returns a reference to the coroutine as a Job.
**/
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> Unit
): Job
!!/**
* Creates a coroutine and returns its future result as
* an implementation of Deferred.
!*/
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> T
): Deferred<T>
71. !!/**
* Launches a new coroutine without blocking the current thread and
* returns a reference to the coroutine as a Job.
**/
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> Unit
): Job
!!/**
* Creates a coroutine and returns its future result as
* an implementation of Deferred.
!*/
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() "-> T
): Deferred<T>
72. Mostofthetimes…
Use launch in a top level coroutines scope
to create a new coroutine
Use async in a nested coroutines scope to
execute tasks in parallel
73. suspend fun topUsers(): List<User> {
val topUsers = api.fetchTopUsers()
storage.save(topUsers)
return topUsers
}1
Fireandforget
74. suspend fun topUsers(): List<User> = coroutineScope {
val topUsers = api.fetchTopUsers()
launch {
storage.save(topUsers)
}2
topUsers
}1
Fireandforget
75. !!/**
* !!...
* When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled.
* This function returns as soon as the given block and
* all its children coroutines are completed.
* !!...
!*/
suspend fun <R> coroutineScope(block: suspend CoroutineScope.() "-> R): R
76. !!/**
* !!...
* When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled.
* This function returns as soon as the given block and
* all its children coroutines are completed.
* !!...
!*/
suspend fun <R> coroutineScope(block: suspend CoroutineScope.() "-> R): R
77. suspend fun topUsers(): List<User> = coroutineScope {
val topUsers = api.fetchTopUsers()
launch {
storage.save(topUsers)
}2
topUsers
}1
Fireandforget
89. val fetch = async {
try {
FetchSuccess(topUsers())
} catch (e: Exception) {
FetchError(e)
}1
}2
val timeout = async {
delay(2000)
Timeout
}3
val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
90. val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
91. val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
when (result) {
}b
92. val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
when (result) {
is FetchSuccess "-> {
timeout.cancel()AAA
updateUi(result.data)
}5
}b
93. val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
when (result) {
is FetchSuccess "-> {
timeout.cancel()AAA
updateUi(result.data)
}5
is Timeout "-> {
storage.load()"?.let {
updateUi(it)
}8
val fetchResult = fetch.await()
(fetchResult as? FetchSuccess)"?.let {
updateUi(it.data)
}9
}a
}b
94. val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
when (result) {
is FetchSuccess "-> {
timeout.cancel()
updateUi(result.data)
}5
is Timeout "-> {
storage.load()"?.let {
updateUi(it)
}8
val fetchResult = fetch.await()
(fetchResult as? FetchSuccess)"?.let {
updateUi(it.data)
}9
}a
is FetchError "-> {
timeout.cancel()
val dataFromCache = storage.load()
if (dataFromCache "!= null) {
updateUi(dataFromCache)
} else {
showErrorMessage()
}6
}7
}b
95. val fetch = async {
try {
FetchSuccess(topUsers())
} catch (e: Exception) {
FetchError(e)
}1
}2
val timeout = async {
delay(2000)
Timeout
}3
val result = select<FetchResult<out List<UserStats"">>> {
fetch.onAwait { it }
timeout.onAwait { it }
}4
when (result) {
is FetchSuccess "-> {
timeout.cancel()
updateUi(result.data)
}5
is Timeout "-> {
storage.load()"?.let {
updateUi(it)
}8
val fetchResult = fetch.await()
(fetchResult as? FetchSuccess)"?.let {
updateUi(it.data)
}9
}a
is FetchError "-> {
timeout.cancel()
val dataFromCache = storage.load()
if (dataFromCache "!= null) {
updateUi(dataFromCache)
} else {
showErrorMessage()
}6
}7
}b
96. class RefreshStrategyTest {
private val updateUiCalls = mutableListOf<Pair<String, Long">>()
private val showErrorCalls = mutableListOf<Long>()
@Test
fun initialLoading() = runBlockingTest {
refresh(
storage = { null },
network = {
delay(1000)
"New data"
},
updateUi = { updateUiCalls.add(it to currentTime) },
showErrorMessage = { showErrorCalls.add(currentTime) }
)3
assertThat(updateUiCalls).containsExactly("New data" to 1000L)
assertThat(showErrorCalls).isEmpty()
}1
}2
97. @Test
fun initialLoading() = runBlockingTest {
refresh(
storage = { null },
network = {
delay(1000)
"New data"
},
updateUi = { updateUiCalls.add(it to currentTime) },
showErrorMessage = { showErrorCalls.add(currentTime) }
)3
assertThat(updateUiCalls).containsExactly("New data" to 1000L)
assertThat(showErrorCalls).isEmpty()
}1
98. @Test
fun initialLoading() = runBlockingTest {
refresh(
storage = { null },
network = {
delay(1000)
"New data"
},
updateUi = { updateUiCalls.add(it to currentTime) },
showErrorMessage = { showErrorCalls.add(currentTime) }
)3
assertThat(updateUiCalls).containsExactly("New data" to 1000L)
assertThat(showErrorCalls).isEmpty()
}1
99. @Test
fun initialLoading() = runBlockingTest {
refresh(
storage = { null },
network = {
delay(1000)
"New data"
},
updateUi = { updateUiCalls.add(it to currentTime) },
showErrorMessage = { showErrorCalls.add(currentTime) }
)3
assertThat(updateUiCalls).containsExactly("New data" to 1000L)
assertThat(showErrorCalls).isEmpty()
}1
100. @Test
fun dataAreLoadedIn5SecondsFromNetwork() = runBlockingTest {
refresh(
storage = { "Old data" },
network = {
delay(5000)
"New data"
},
updateUi = { updateUiCalls.add(it to currentTime) },
showErrorMessage = { showErrorCalls.add(currentTime) }
)
assertThat(updateUiCalls)
.containsExactly(
"Old data" to 2000L,
"New data" to 5000L
)
assertThat(showErrorCalls).isEmpty()
}
102. Wrappingup
Define main safe functions
Use launch in a top level coroutines scope
Use async in a nested coroutines scope
Never catch an exception inside a
coroutinesScope
103. Links&contacts
Coroutines on Android (part I): Getting the background
medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb
Async code using Kotlin Coroutines
proandroiddev.com/async-code-using-kotlin-coroutines-233d201099ff
Managing exceptions in nested coroutine scopes
proandroiddev.com/managing-exceptions-in-nested-coroutine-scopes-9f23fd85e61
@fabioCollini
linkedin.com/in/fabiocollini
github.com/fabioCollini
medium.com/@fabioCollini