There is a very common pattern using CountDownLatch
when developing with Android, sometimes you want to make an async implementation to be sync when dealing with BroadcastReceivers
and CountDownLatch
is pretty handy. The AOSP is filled with CountDownLatch(1)
.
private suspend fun yourSuspendMethod() {
val job = GlobalScope.async {
val latch = CountDownLatch(1)
val watcher = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if // your logic
latch.countDown()
}
}
try {
mContext?.registerReceiver(watcher, IntentFilter(...))
//call a method that will trigger the broadcast receiver
if (!latch.await(5, TimeUnit.SECONDS)) {
throw Exception("Failed .... on latch's timeout")
}
} finally {
mContext?.unregisterReceiver(watcher)
}
}
job.await()
}
The problem of the latch is that when you call latch.await
it will stop the current thread, so if this is coming from a Main thread, the Main will wait and it will timeout because it didn't give a chance for the receiver to be called. A way of solving this is by injecting the same/new context of the caller/launch. It will allow you to unit test and synchronize the context of the caller. If you decide to do that, your implementation will become a bit more complex and you will not be using the full power of the coroutine, because you will be creating extra threads.
So, the solution would be using a combination of withTimeout
+ suspendCancellableCoroutine
, you can use this extension:
suspend inline fun <T> suspendCoroutineWithTimeout(
timeout: Long,
crossinline block: (Continuation<T>) -> Unit
) = withTimeout(timeout) {
suspendCancellableCoroutine(block = block)
}
and your method would look like this:
suspend fun yourSuspendMethod() {
var watcher: BroadcastReceiver? = null
try {
suspendCoroutineWithTimeout<Boolean>(TimeUnit.SECONDS.toMillis(5)) {
watcher = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if // your logic
it.resume(true)
}
}
context?.registerReceiver(watcher, IntentFilter(...))
//call a method that will trigger the broadcast receiver
}
} finally {
context?.unregisterReceiver(watcher)
}
}
That would be it. Now coroutine would do its magic without stoping the caller thread and when the job gets canceled the timeout will also cancel your block.