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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With