Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to suspendCoroutine in a "by lazy" initializer? I get errors of "runBlocking is not allowed in Android main looper thread"

I've got much of my app working fine with "by lazy" initializers because everything magically happens in the order that is necessary.

But not all of the initializers are synchronous. Some of them are wrapping callbacks, which means I need to wait until the callback happens, which means I need runBlocking and suspendCoroutine.

But after refactoring everything, I get this IllegalStateException: runBlocking is not allowed in Android main looper thread

What? You can't block? You're killing me here. What is the right way if my "by lazy" happens to be a blocking function?

private val cameraCaptureSession: CameraCaptureSession by lazy {
    runBlocking(Background) {
        suspendCoroutine { cont: Continuation<CameraCaptureSession> ->
            cameraDevice.createCaptureSession(Arrays.asList(readySurface, imageReader.surface), object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    cont.resume(session).also {
                        Log.i(TAG, "Created cameraCaptureSession through createCaptureSession.onConfigured")
                    }
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {
                    cont.resumeWithException(Exception("createCaptureSession.onConfigureFailed")).also {
                        Log.e(TAG, "onConfigureFailed: Could not configure capture session.")
                    }
                }
            }, backgroundHandler)
        }
    }
}

Full GIST of the class, for getting an idea of what I was originally trying to accomplish: https://gist.github.com/salamanders/aae560d9f72289d5e4b49011fd2ce62b

like image 266
Benjamin H Avatar asked Aug 14 '18 05:08

Benjamin H


1 Answers

It is a well-known fact that performing a blocking call on the UI thread results in a completely frozen app for the duration of the call. The documentation of createCaptureSession specifically states

It can take several hundred milliseconds for the session's configuration to complete, since camera hardware may need to be powered on or reconfigured.

It may very easily result in an Application Not Responding dialog and your app being killed. That's why Kotlin has introduced an explicit guard against runBlocking on the UI thread.

Therefore your idea to start this process just in time, when you have already tried to access cameraCaptureSession, cannot work. What you must do instead is wrap the code that accesses it into launch(UI) and turn your val into a suspend fun.

In a nutshell:

private var savedSession: CameraCaptureSession? = null

private suspend fun cameraCaptureSession(): CameraCaptureSession {
    savedSession?.also { return it }
    return suspendCoroutine { cont ->
        cameraDevice.createCaptureSession(listOf(readySurface, imageReader.surface), object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) {
                savedSession = session
                Log.i(TAG, "Created cameraCaptureSession through createCaptureSession.onConfigured")
                cont.resume(session)
            }

            override fun onConfigureFailed(session: CameraCaptureSession) {
                Log.e(TAG, "onConfigureFailed: Could not configure capture session.")
                cont.resumeWithException(Exception("createCaptureSession.onConfigureFailed"))
            }
        })
    }
}

fun useCamera() {
    launch(UI) {
        cameraCaptureSession().also { session ->
            session.capture(...)
        }
    }
}

Note that session.capture() is another target for wrapping into a suspend fun.

Also be sure to note that the code I gave is only safe if you can ensure that you won't call cameraCaptureSession() again before the first call has resumed. Check out the followup thread for a more general solution that takes care of that.

like image 101
Marko Topolnik Avatar answered Oct 20 '22 08:10

Marko Topolnik