Reputation: 1299
I have two questions about this topic. I will use these in android with use case classes and i try to implement an architecture similar to this https://www.youtube.com/watch?v=Sy6ZdgqrQp0 but i need some answers.
1) I have a deferred with async builder and when i cancel job then the other chains cancelled too. This code prints "Call cancelled". But i am not sure that if i am doing correct.
fun main(args: Array<String>) = runBlocking<Unit> {
val job = GlobalScope.launch {
println(getUser())
}
job.cancelAndJoin()
}
suspend fun getUser() = getUserDeferred().await()
suspend fun getUserDeferred() = coroutineScope {
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build()
val call = OkHttpClient().newCall(request)
val deferred = async(Dispatchers.IO) {
val body = call.execute()
body.body()?.string() ?: ""
}
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
println("Call cancelled")
call.cancel()
}
}
deferred
}
2) I can't find a way to cancel this one. I want to use this in retrofit2 call adapter, is there any better way to handle this case.
fun main(args: Array<String>) = runBlocking<Unit> {
val job = GlobalScope.launch {
println(getUser1())
}
job.cancelAndJoin()
}
suspend fun getUser1() = getUser1Deferred().await()
fun getUser1Deferred(): Deferred<String> {
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build()
val call = OkHttpClient().newCall(request)
val deferred = CompletableDeferred<String>()
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
deferred.complete("Error")
}
override fun onResponse(call: Call, response: Response) {
deferred.complete(response.body()?.string() ?: "Error")
}
})
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
println("Call cancelled")
call.cancel()
}
}
return deferred
}
Upvotes: 4
Views: 7427
Reputation: 663
The reason the 2nd case is not cancelling is because you are using CompletableDeferred
. It isn't launched as a coroutine so isn't a child of your parent coroutine. So if you cancel the parent it will not cancel the deferred.
It works in the first case because async
starts a new child coroutine which is linked to the parent. when you cancel either one they both get cancelled.
In order to link the Deferred
to your parent Job you would need a reference to it and use invokeOnCompletion
var deferred : Deferred<Void>? = null
launch {
deferred = retroService.someDeferredCall()
deferred.await()
}.invokeOnCompletion {
//job was cancelled. Probably activity closing.
if(it is CancellationException) {
deferred?.let { it.cancel() }
}
}
Not terribly pretty but should get the job done.
Upvotes: 0
Reputation: 200256
You should avoid the first approach because it blocks a thread in a thread pool. Using the second approach you can propagate cancellation both ways. If you cancel the Deferred
it will cancel the call, and if the call fails, it will cancel the Deferred
with the exception it got.
fun getUserAsync(): Deferred<String> {
val call = OkHttpClient().newCall(Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build())
val deferred = CompletableDeferred<String>().apply {
invokeOnCompletion {
if (isCancelled) {
call.cancel()
}
}
}
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
deferred.complete(response.body()?.string() ?: "Error")
}
override fun onFailure(call: Call, e: IOException) {
deferred.cancel(e)
}
})
return deferred
}
However, going the Deferred
route is probably a red herring. If you are cancelling it, the underlying reason is that you're bailing out of the whole task you're doing. You should instead cancel the whole coroutine it runs in. If you properly implement structured concurrency, everything will happen automatically if your activity gets destroyed.
So my recommendation is to use this code:
suspend fun getUser() = suspendCancellableCoroutine<String> { cont ->
val call = OkHttpClient().newCall(Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build())
cont.invokeOnCancellation {
call.cancel()
}
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
cont.resume(response.body()?.string() ?: "Error")
}
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
})
}
If you absolutely need the Deferred
because you're running it concurrently in the background, it's easy to do using the above:
val userDeferred = this.async { getUser() }
Where I assume this
is your activity, which is also a CoroutineScope
.
Upvotes: 5