Coroutines Playground: Concurrency + Timeout + Exception Handling
You may have read about different concepts of coroutines, This article will put all those concepts together (okay not all but some 😉). We are going to learn about Coroutines using a problem statement.
Problem Statement
You have APIs to resolve User Details (User
), User Friends List (List<Friends>
) and User Activity List ( List<UserActivity>
).
To get the user, call the getUserData()
function. To get a user friends list and user activity list using id
resolved from User
.
All these functions are suspended and need to be called from a coroutine context.
Implement a function fetchData()
where it must satisfy the following requirements:
- When
fetchUserData()
throws an error, handle the exception. - When
fetchUserData()
returnUser
, CallfetchFriends()
andfetchUserActivity()
usinguser.id
then aggregate the result. - When
fetchFriends()
call takes longer than 2000ms, and return a empty friends array. - When
fetchFriends()
throws an exception, and return a empty friends array. - When
fetchUserActivity()
call takes longer than 2000ms, returns an empty array. - When
fetchUserActivity()
throws an exception, return an empty user activity array.
Solution
Note : Here API call functions are mocked with dummy data for demonstration purpose
First, we get user data using fetchUserData()
with a dummy, a user is returned to simulate an API call. Added try catch for any handling exception that comes while making the fetchUserData()
call.
class CoroutinePlayground {
suspend fun fetchData() {
try {
val result = withContext(Dispatchers.IO) { fetchUserData() }
} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
private suspend fun fetchUserData(): User {
delay(1000)
return User()
}
}
// demo user
data class User(val name: String = "Demo user", val id: String = "123")
Next, let’s resolve fetchFriends()
and fetchUserActivity()
using the user data we got from fetchUserData()
. Added the calls in supervisorScope
and to achieve concurrency we are making use of async
.
why supervisorScope ?: Unlike coroutineScope, a failure of a child does not cause this scope to fail and does not affect its other children.
class CoroutinePlayground {
suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }
val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }
} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}
private suspend fun fetchUserData(): User {
delay(1000)
return User()
}
// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}
// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}
// demo Friends
data class Friends(val name: String)
// demo user activity
data class UserActivity(val type: String)
// demo user
data class User(val name: String = "Demo user", val id: String = "123")
The next step requires timeout and exception handling when making the fetchFriends()
. To cancel a Coroutine in case of a timeout we can make use of withTimeout(time)
. withTimeout()
throws TimeoutCancellationException
on timeout.
class CoroutinePlayground {
suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }
val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }
val friendList = try {
withTimeout(2000) {
friendResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
}
} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}
private suspend fun fetchUserData(): User {
delay(1000)
return User()
}
// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}
// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}
// demo Friends
data class Friends(val name: String)
// demo user activity
data class UserActivity(val type: String)
// demo user
data class User(val name: String = "Demo user", val id: String = "123")
Similarly, we can handle timeout and exceptions forfetchUserActivity()
API call.
class CoroutinePlayground {
suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }
val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }
val friendList = try {
withTimeout(2000) {
friendResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
}
val activityList = try {
withTimeout(2000) {
activityResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "activityList: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "activityList: ${e.message}")
emptyList()
}
Log.d(TAG, "friendList: ${friendList.size}")
Log.d(TAG, "activityList: ${activityList.size}")
} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}
private suspend fun fetchUserData(): User {
delay(1000)
return User()
}
// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}
// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}
// demo Friends
data class Friends(val name: String)
// demo user activity
data class UserActivity(val type: String)
// demo user
data class User(val name: String = "Demo user", val id: String = "123")
And this code demonstrates the solution for the above problem statement.
To simulate the exception handling and timeout behavior, you try throwing an exception from fetchUserData()
, fetchUserActivity()
and fetchFriends()
.
You can also add a delay in fetchUserActivity()
and fetchFriends()
to see the timeout behavior.
Hope this article helped you 😄
Feel free to provide any feedback, Happy coding :)
Read about object expression in Kotlin