Is it somehow possible to await a continuation?
# coroutines
p
Is it somehow possible to await a continuation?
a
You can convert it to a deferred. But what’s your use case? Suspended functions operate sequentially unless otherwise specified
o
You could
async { cont.resume() }
I suppose, but it doesn't return a value, so you probably want to use
launch
&
join
. Seems like an XY problem though.
p
How can I convert it to a deferred @Allan Wang?
I use coroutines to request android permissions and store the continuations in a map for each permission request code. Now I found a bug resulting in the fact that you can't request 2 permissions at the same time, so when someone requests a permission, I need to await for the old continuations to complete first.
o
I think you might have terms mixed up here -- a continuation doesn't have a strict sense of completion, that's all tied to
Job
, which you can already
join
on. A better model for your case might be to use an
actor
for each code, so that all permission requests are automatically processed sequentially. Then you don't have to wait for anything.
p
Why would that be better?
suspendCancellableCoroutine
gives me a continuation and that's exactly what I need
e
@Paul Woitaschek For your use-case a
CompletableDeferred
whould be a much better fit. Use and store them instead of continuations, and you'll have no problem waiting for the same one multiple times.
p
@elizarov How would that look like? In the end the signature would need to look like this:
suspend fun request(permission: String): PermissionResult
So I need to store what kind of permission I was requesting, right?
e
Something like this:
Copy code
suspend fun request(permission: String) = 
    mapPermissionToDeferred.getOrPut(permsision) { 
        requestPermission(permission)
    }.await()
Or even better:
Copy code
uspend fun request(permission: String) = 
    mapPermissionToDeferred.getOrPut(permsision) { 
        appScope.async { requestPermission(permission) }
    }.await()
where:
Copy code
mapPermissionToDeferred: MutableMap<String, Deferred<PermissionResult>>
suspend fun requestPermission: (permission: String): PermissionResult
(no need to explicitly use
CompletableDeferred
with the last approach -- just use
async
with a proper application-level scope)
p
And how does that solve the problem of concurrent requests?
o
hmm, looking into the android permission request model, I think
CompletableDeferred
is on the right track. Basically, since (If I'm interpreting your words correctly) you can only call
requestPermissions
one at a time, you can store the
requestCode
& a
CompletableDeferred
, then in your
suspend fun request
, if it's non-null,
await
it first. Then set the request code, a new
CompletableDeferred
, and call
requestPermission
. Then await your new deferred. You might need to use a
Mutex
here to ensure it's concurrently-safe, or box the requests in a data object and use
actor
to automatically sequential-ize* it. In
onRequestPermissionsResult
, you can check the code against the field, then complete the deferred. No Map needed.
e
Assyming that you only call
suspend fun request
from your main thread (you can wrap its body into
withContext(Dispatchers.Main) { ... }
to be sure, you'll never get two concurrent requests to the same permission, since the first one will get stored in the map via
getOrPut
p
But I don't want to store the permission result permanently because the result might change
o
there's also that option
you can always
null
out the field before setting the result. that's perfectly fine GC-wise
p
to be sure, you'll never get two concurrent requests to the same permission
No, I must be sure to never get two concurrent requests ever, unregarding of the permission type
e
@Paul Woitaschek In this case it would be prudent to create a global actor that makes request. It would check incoming requsts, complete, then, then remove deferred from the map, go to the next request:
p
And what is the benefit with the
CompletableDeferred
?
This already looks pretty straight forward to me
e
Copy code
suspend fun requestPermission: (permission: String, result: CompletableDeferred<PermissionResult>): PermissionResult = permission.send(permission to PermissionResult)
where
Copy code
permissionActor = actor<Pair<String, PermissionResult>> {
    for ((permission, result) in channel) {
        val permission = getIt(...)
        result.complete(permission)
        mapPermissionToDeferred.remove(permission)
        // ^^ this way future requests will request it again
    }
}
If it already works for you, then you can just change
CancellableContinuation
with
CompletableDeferred
and instead of
resume
call
complete
.
Instead of
suspendCancellableCoroutine<Unit> { cont -> ... }
you can do:
Copy code
val d = CompletableDeferred<Unit> /// why unit? 
...
d.await()
I don't see how you make sure there are no concurrent requests in your code, though.
o
My idea translates roughly like this: https://gist.github.com/kenzierocks/bc80dead52b71448477ea2ff21cf0ff4 you may not need the mutex depending on the threading model
and of course, you can set
requestPermissionsDeferred = null
in
onRequestPermissionsResult
, if you wish to clean up
p
And calling
actor.send
waits till the actor is empty? How do I get a result from it?
o
send a
CompletableDeferred
. have the actor complete it
l
@Paul Woitaschek Did you try Splitties permissions? It should handle the situation where you request multiple permissions concurrently by spawning another Activity. Here's the link: https://github.com/LouisCAD/Splitties/tree/master/modules/permissions