Skip to content

Instantly share code, notes, and snippets.

@jairobjunior
Last active December 16, 2019 21:43
Show Gist options
  • Save jairobjunior/8a0e4251ba81a4f13da2199abe51d302 to your computer and use it in GitHub Desktop.
Save jairobjunior/8a0e4251ba81a4f13da2199abe51d302 to your computer and use it in GitHub Desktop.
Android / Kotlin - A coroutine version of CountDownLatch 1 and BroadcastReceivers

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment