The general idea for this exercise was to practice two kinds of wrapping callback-based APIs into suspending functions: one that throws exceptions on errors, and another that returns a Result type. This is how it can be solved:
Alternatively, callback lambda arguments can be explicitly named:
class FetchTasksUseCase(
private val callbackUseCase: FetchTasksCallbackUseCase
) {
@Throws(ApiException::class)
suspend fun fetchTasks(): List<Task> =
suspendCancellableCoroutine { cont ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { result -> cont.resume(result) },
onError = { error -> cont.resumeWithException(error) }
)
cont.invokeOnCancellation { cancellable.cancel() }
}
suspend fun fetchTasksResult(): Result<List<Task>> =
suspendCancellableCoroutine { cont ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { result -> cont.resume(Result.success(result)) },
onError = { error -> cont.resume(Result.failure(error)) }
)
cont.invokeOnCancellation { cancellable.cancel() }
}
}
It is also acceptable if we define one function in terms of the other. We can define fetchTasks as fetchTasksResult().getOrThrow():
class FetchTasksUseCase(
private val callbackUseCase: FetchTasksCallbackUseCase
) {
@Throws(ApiException::class)
suspend fun fetchTasks(): List<Task> = fetchTasksResult().getOrThrow()
suspend fun fetchTasksResult(): Result<List<Task>> =
suspendCancellableCoroutine { cont ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { result -> cont.resume(Result.success(result)) },
onError = { error -> cont.resume(Result.failure(error)) }
)
cont.invokeOnCancellation { cancellable.cancel() }
}
}
Alternatively, you can define fetchTasksResult using runCatching, but since runCatching catches CancellationException, we need to explicitly rethrow it (this problem will be discussed next week).
class FetchTasksUseCase(
private val callbackUseCase: FetchTasksCallbackUseCase
) {
@Throws(ApiException::class)
suspend fun fetchTasks(): List<Task> = suspendCancellableCoroutine { continuation ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { tasks ->
continuation.resume(tasks)
},
onError = { throwable ->
continuation.resumeWithException(throwable)
}
)
continuation.invokeOnCancellation {
cancellable.cancel()
}
}
suspend fun fetchTasksResult(): Result<List<Task>> =
runCatching { fetchTasks() }
.onFailure { if(it is CancellationException) throw it }
}
Example solution in playground
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class FetchTasksUseCase(
private val callbackUseCase: FetchTasksCallbackUseCase
) {
@Throws(ApiException::class)
suspend fun fetchTasks(): List<Task> =
suspendCancellableCoroutine { cont ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { cont.resume(it) },
onError = { cont.resumeWithException(it) }
)
cont.invokeOnCancellation { cancellable.cancel() }
}
suspend fun fetchTasksResult(): Result<List<Task>> =
suspendCancellableCoroutine { cont ->
val cancellable = callbackUseCase.fetchTasks(
onSuccess = { cont.resume(Result.success(it)) },
onError = { cont.resume(Result.failure(it)) }
)
cont.invokeOnCancellation { cancellable.cancel() }
}
}
interface FetchTasksCallbackUseCase {
fun fetchTasks(
onSuccess: (List<Task>) -> Unit,
onError: (Throwable) -> Unit
): Cancellable
}
fun interface Cancellable {
fun cancel()
}
data class Task(val name: String, val priority: Int)
class ApiException(val code: Int, message: String): Throwable(message)
class FetchTasksTests {
val someTasks = listOf(Task("1", 123), Task("2", 456))
val someException = ApiException(500, "Some exception")
@Test
fun `fetchTasks should resume with result`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var result: List<Task>? = null
// when
launch {
result = useCase.fetchTasks()
}
// then
runCurrent()
assertEquals(null, result)
fakeFetchTaskCallback.onSuccess?.invoke(someTasks)
runCurrent()
assertEquals(someTasks, result)
}
@Test
fun `fetchTasks should resume with exception`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var exception: Throwable? = null
// when
launch {
try {
useCase.fetchTasks()
} catch (e: Throwable) {
exception = e
}
}
// then
runCurrent()
assertEquals(null, exception)
fakeFetchTaskCallback.onError?.invoke(someException)
runCurrent()
assertEquals(someException, exception)
}
@Test
fun `fetchTasks should support cancellation`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var cancelled = false
fakeFetchTaskCallback.onCancelled = { cancelled = true }
// when
val job = launch {
useCase.fetchTasks()
}
// then
runCurrent()
assertEquals(false, cancelled)
job.cancel()
assertEquals(true, cancelled)
}
@Test
fun `fetchTasksResult should resume with result`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var result: Result<List<Task>>? = null
// when
launch {
result = useCase.fetchTasksResult()
}
// then
runCurrent()
assertEquals(null, result)
fakeFetchTaskCallback.onSuccess?.invoke(someTasks)
runCurrent()
assertNotNull(result)
assertTrue(result!!.isSuccess)
assertEquals(someTasks, result!!.getOrNull())
}
@Test
fun `fetchTasksResult should resume with failure`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var result: Result<List<Task>>? = null
// when
launch {
result = useCase.fetchTasksResult()
}
// then
runCurrent()
assertEquals(null, result)
fakeFetchTaskCallback.onError?.invoke(someException)
runCurrent()
assertNotNull(result)
assertTrue(result!!.isFailure)
assertEquals(someException, result!!.exceptionOrNull())
}
@Test
fun `fetchTasksResult should support cancellation`() = runTest {
// given
val fakeFetchTaskCallback = FakeFetchTasksCallbackUseCase()
val useCase = FetchTasksUseCase(fakeFetchTaskCallback)
var cancelled = false
fakeFetchTaskCallback.onCancelled = { cancelled = true }
// when
val job = launch {
useCase.fetchTasksResult()
}
// then
runCurrent()
assertEquals(false, cancelled)
job.cancel()
assertEquals(true, cancelled)
}
class FakeFetchTasksCallbackUseCase: FetchTasksCallbackUseCase {
var onSuccess: ((List<Task>) -> Unit)? = null
var onError: ((Throwable) -> Unit)? = null
var onCancelled: (()->Unit)? = null
override fun fetchTasks(onSuccess: (List<Task>) -> Unit, onError: (Throwable) -> Unit): Cancellable {
this.onSuccess = onSuccess
this.onError = onError
return Cancellable { onCancelled?.invoke() }
}
}
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.