When creating a method that can be cancelled, is t...
# coroutines
a
When creating a method that can be cancelled, is the correct approach to use
suspendCancellableCoroutine
and to check for
isCancelled
whenever the method can be cancelled and then to throw an exception or call
resumeWithException
? Is there any other way in doing this?
g
If coroutine is cancelled you shouldn't resume
Could you show an example of method that you want to make cancellable. There are a few approaches depending on case
a
I’m mainly asking since coroutine cancellations have to be “cooperative”. As an example, say I want to compute a list. Each item has some action, and I want to check for a cancellation before continuing. If my method is cancelled or completely executed, I will return the current list. How would I do that?
Copy code
fun getList(): List<Int> {
    val list = mutableListOf<Int>
    list.add(/* computed value */)
    // If cancelled return list
    list.add(/* computed value */)
    // If cancelled return list
    ...
    return list
}
In the example above, I would wrap everything in
suspendCancellableCoroutine
and call the continuation, since that’s the only way I know how. In another situation, I might want to propagate a cancellation exception when the current block is incomplete but cancelled. Here, I can still use the continuation pattern, but I’d rather not create my own exception to resume with if there’s another way. My assumption is that by cancelling a job, a cancellation exception should already exist somewhere, so I shouldn’t have to make another one.
g
You cannot return of coroutine is cancelled, it will be just ignored
a
What if it was wrapped in a noncancellable context? A use case could be that you want to generate data until you choose to stop computing new ones.
b
Why not just use produce/sequence to lazily produce data
a
I had a bad example there. Below is the actual method I want to convert: https://github.com/AllanWang/Frost-for-Facebook/blob/697d01882ba8b1dbb85484ba3bf6e810e32448fc/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt#L100 It’s a long running execution, and I check for cancellation at each stage. I want to convert it to use suspension rather than a callback, where it will return true or false, and stop immediately at each stage if it’s cancelled. I was wondering if there’s a better way of propagating cancellation than checking if the scope is active and resuming with my own exception if it isn’t
b
You could use
yield
in any
suspend fun
at points of cancellation
Copy code
suspend fun doLongWork() {
    repeat(10) {
        doSomethingOne()
        yield() // check if it should cancel or be re-dispatched
    }
    doSomethingTwo()
    yield()
    ...
    doSomethingN()
    yield()
    doFinal()
}
1
a
Thanks! That seems to be exactly what I was looking for
g
yield() is suspention point, you also can use isActive property (doesn't require suspending, but will not re-dispatch coroutine)
a
With isActive, I have to resume with my own exception though right? Me having to write my own is the main reason I'd like to avoid it. I'm currently okay with the re-dispatch
g
No, you can just do early return and that's all, you shouldn't throw any exceptions in you adapter
But I have nothing against yeild(), often code with yield() is much easier to write
Even better if you call other suspend functions, in this case you have cancellation points for free, without additional yield()/isActive boilerplate. So you may be want just extract separate phases to own suspend functions
I also usually use isActive with loops, where it more natural and efficient than yeild
a
I get the use case for loops, but in cases like
suspendCancellableCoroutine
, returning a default value isn’t equivalent to
resumeWithException
is it?
Following your response of “free” cancellation points, does that mean I don’t have to check for yield if I use
withContext
? A lot of my checks are to ensure that my android context is still valid before executing something that uses it on my main thread. Eg:
Copy code
attempt = launch(<http://Dispatchers.IO|Dispatchers.IO>) {
    try {
        val data = parser.parse(FbCookie.webCookie)
        yield()
        withContext(Dispatchers.Main) {
            loading.dismiss()
            createEmail(parser, data?.data)
        }
    } catch (e: Exception) {
        createEmail(parser, "Error: ${e.message}")
    }
}
g
Why do you check that context is valid? It doesn't look like necessary check if you have proper cancellation But even if you have some case to check context (like isAdded for fragment), you may simplify your code and extract this check to suspend extension function which cancels parent if context is invalid, similar approach used in Anko for weak references
yeild before withContext is useless, this exactly what I'm talking about, if you use suspend functions each of them become implicit cancellation check
👍 1