https://kotlinlang.org logo
#coroutines
Title
# coroutines
m

Mohamed

02/11/2021, 4:10 PM
How to use
withTimeout
inside
suspendCoroutine
? something like:
Copy code
suspendCoroutine{ cont->
  withTimeout(1000){
    // DO something
    cont.resumeWithException(Exception("Timeout"))
  }
}
g

gildor

02/11/2021, 4:18 PM
You cannot do this easily, suspendCotoutine lambda is not a suspend block, you of course can run a separate coroutine with launch on some scope as background job, but this approach requires manual handling of cancellation and kinda smells Could you show some example what you are trying to achieve? Maybe better write coroutine adapter separately and then add wrapper on it with this timeout logic
z

Zach Klippenstein (he/him) [MOD]

02/11/2021, 4:41 PM
Unless I’m missing something (likely, since I haven’t had coffee yet), you could just invert the wrapping - lift the withTimeout call to be just outside your suspendCoroutine, and use suspendCancellableCoroutine instead. withTimeout just cancels the block inside it. It’s no different than any other coroutine cancellation. And it’s a good practice to almost always use suspendCancellableCoroutine instead of suspendCoroutine and ensure that your non-coroutines code handles cancellation gracefully anyway. So if your code handles cancellation properly in general, and you move the withTimeout out, then the continuation will be cancelled after the timeout and you can handle that in your non-coroutine code the same way you’d handle any other cancellation.
m

Mohamed

02/11/2021, 4:47 PM
I am trying to do
Copy code
private suspend fun callSomeGooglePlayServicesApi(): List<String> = suspendCoroutine { cont ->
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                cont.resume(intent.getArrayList())
                context.unregisterReceiver(this)
            }
        }

        context.registerReceiver(receiver)

        googleApiClient.someTask()
            .addOnSuccessListener {
                withTimeout(15000) {
                    context.unregisterReceiver(preAuthKeysReceiver)
                    cont.resumeWithException(Exception("Time out"))

                }
            }.addOnFailureListener {
                cont.resumeWithException(it)
                context.unregisterReceiver(receiver)
            }
}
z

Zach Klippenstein (he/him) [MOD]

02/11/2021, 5:15 PM
Why do you want to resume the continuation with an exception timeout if the task completes succesfully?
Also the docs on
suspendCancellableCoroutine
include a simple code sample that demonstrates how to use it pretty well.
👍 1
m

Mohamed

02/11/2021, 5:21 PM
I just want to timeout if the receiver did not get called at all, so I added this timeout to make it will not hang forever
Thanks for the docs. it is very helpful
z

Zach Klippenstein (he/him) [MOD]

02/11/2021, 5:23 PM
i see why you want the timeout, but
withTimeout
will already throw its own timeout exception. Your code is throwing your own exception when the google task completes successfully, but i don’t understand why you’d want that to look like a timeout to the caller.
I would also factor this code a little more. Your broadcast receiver stuff could be its own suspend function:
Copy code
private suspend fun awaitReceivedBroadcast(): Intent = 
  suspendCancellableCoroutine { cont ->
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
          context.unregisterReceiver(this)
          cont.resume(intent)
        }
      }

      context.registerReceiver(receiver)
      cont.invokeOnCancellation {
        context.unregisterReceiver(receiver)
      }
    }
  }
d

Dominaezzz

02/11/2021, 5:26 PM
I think what you want here is a Flow with a call to
first
in a withTimeout. I'll try and write something. The above will do okay.
z

Zach Klippenstein (he/him) [MOD]

02/11/2021, 5:26 PM
There’s also a library that provides integration of the Task api with coroutines: https://developers.google.com/android/guides/tasks#kotlin_coroutine
then your timeout and task handling logic can probably be much more concise and not littered with callbacks
Copy code
private suspend fun callSomeGooglePlayServicesApi(): List<String> =
  withTimeout(15_000) {
    coroutineScope {
      // Need to launch at least one of these calls into its own coroutine
      // so they run concurrently.
      val list = awaitReceivedBroadcast().getArrayList()
      // …
      val taskResult = googleApiClient.someTask().await()

      // Do something with the result.
    }
  }
d

Dominaezzz

02/11/2021, 5:33 PM
Alternative refactor.
Copy code
private suspend fun awaitReceivedBroadcast(): Intent =
    callbackFlow<Intent> {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                send(intent)
            }
        }
        context.registerReceiver(receiver)
        awaitClose { context.unregisterReceiver(receiver) }
    }.first()
z

Zach Klippenstein (he/him) [MOD]

02/11/2021, 5:41 PM
why use flow? that’s just the same thing with more indirection. Also i don’t think you can call
send
from
onReceive
since it’s a suspend function, you’d need
sendBlocking
d

Dominaezzz

02/11/2021, 5:44 PM
I'm just being overly cautious about
cont.resume
being called more than once. (Yeah should be `sendBlocking`/`offer` )
g

gildor

02/12/2021, 6:13 AM
I don't think that Flow is needed for this adapter, even if multiple resume may be called it easy to protect from it using simple flag Using official adapter as Zach suggested + withTimeout wrapper (it it needed) looks as the beat solution
u

uli

02/12/2021, 9:18 AM
General rule: Only do the most basic callback wrapping in
suspend(Cancelable)Coroutine
. All further business logic like timeouts and so on are a lot easier then to do on top the suspend function
☝️ 2
12 Views