Existing 3-function callback to Kotlin Coroutines
Solution 1
In this particular case you can use a general approach to convert a callback-based API to a suspending function via suspendCoroutine
function:
suspend fun CameraManager.openCamera(cameraId: String): CameraDevice? =
suspendCoroutine { cont ->
val callback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cont.resume(camera)
}
override fun onDisconnected(camera: CameraDevice) {
cont.resume(null)
}
override fun onError(camera: CameraDevice, error: Int) {
// assuming that we don't care about the error in this example
cont.resume(null)
}
}
openCamera(cameraId, callback, null)
}
Now, in your application code you can just do manager.openCamera(cameraId)
and get a reference to CameraDevice
if it was opened successfully or null
if it was not.
Solution 2
Use suspendCancellableCoroutine instead of suspendCoroutine with proper exception handling
suspend fun CameraManager.openCamera(cameraId: String): CameraDevice? =
suspendCancellableCoroutine { cont ->
val callback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cont.resume(camera)
}
override fun onDisconnected(camera: CameraDevice) {
cont.resume(null)
}
override fun onError(camera: CameraDevice, error: Int) {
// Resume the coroutine by throwing an exception or resume with null
cont.resumeWithException(/* Insert a custom exception */)
}
}
openCamera(cameraId, callback, null)
}
It is preferable to always choose suspendCancellableCoroutine to handle cancellation of the coroutine scope, or to propagate cancellation from the underlying API. Source with other great examples
Benjamin H
Updated on December 31, 2021Comments
-
Benjamin H over 2 years
I have a general question with a specific example: I'd like to use Kotlin coroutine magic instead of callback hell in Android when taking a picture.
manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(openedCameraDevice: CameraDevice) { println("Camera onOpened") // even more callbacks with openedCameraDevice.createCaptureRequest().... } override fun onDisconnected(cameraDevice: CameraDevice) { println("Camera onDisconnected") cameraDevice.close() } ...
How would I convert that to something less ugly? Is it possible to take an average callback with three or so functions, and turn it into a promise-chain by designating the primary flow as the promise-result path? And if so, should/do I use coroutines to make it async?
I'd love something with async and .await that would result in
manager.open(cameraId).await().createCaptureRequest()
I'm trying to do it through something like the following, but I don't think I'm using
CompletableDeferred
right!suspend fun CameraManager.open(cameraId:String): CameraDevice { val response = CompletableDeferred<CameraDevice>() this.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(cameraDevice: CameraDevice) { println("camera onOpened $cameraDevice") response.complete(cameraDevice) } override fun onDisconnected(cameraDevice: CameraDevice) { response.completeExceptionally(Exception("Camera onDisconnected $cameraDevice")) cameraDevice.close() } override fun onError(cameraDevice: CameraDevice, error: Int) { response.completeExceptionally(Exception("Camera onError $cameraDevice $error")) cameraDevice.close() } }, Handler()) return response.await() }
-
Benjamin H over 6 yearsI'm not clear: what does this buy me? It calls the callbacks, so I still need to provide some sort of callback, so what it did was split out the single Callback (that holds 2 functions) into calling a method and passing in 2 distinct functions. Unless that would enable
.await()
? -
GetSwifty over 6 yearsAt the point of use you're passing lamdas or function references rather than having to create a whole object. Chances are the callbacks are already async. Are you just wanting to make a Class that manages the whole process?
-
Benjamin H over 6 yearsThe callbacks are async, but I'm trying to worm my way out of callback-hell and into something where I can chain functions for the default-path. Like
manager.open(cameraId).capture().saveFile()
Which requires me to block the manager.open(cameraId) call on some value getting filled (which I think should be with CompletableDeferred?) to tie it all together. -
Benjamin H over 6 yearsgithub.com/Kotlin/kotlin-coroutines/blob/master/… YES! Thank you!!
-
Bolein95 almost 6 yearshow do you handle coroutine cancellation in this case?
-
Gábor almost 5 years@Bolein95: by using
suspendCancellableCoroutine
instead. -
Roman Elizarov about 4 yearsYou can choose to represent your error with exception and do
cont.resumeWithException(CameraException(error))
or represent your error with a special result type and docont.resume(CameraErrorResult(error))
. -
Willi Mentzel about 4 yearsthis is just perfect! I <3 coroutines! :)
-
A. Steenbergen about 4 yearsNote that this will crash if the callback fires more than once, e.g. in some cases when rotating the device
-
DarkNeuron over 3 yearsClassic example of indirection, where the problem is just moved under the guise of being fixed. We all do that from time to time. At least I do.
-
DarkNeuron over 3 years@RomanElizarov Is it possible to expand your example to a scenario where you first add your listeners, and then run an
init
orstart
function. For example using the above code, lets assume nothing would happen until you runCameraManager.start()
. How would chaining work in such a case? Hope my question makes sense. -
Anigif about 3 yearsWouldn't it always be preferable to use
suspendCancellableCoroutine
in most cases? -
Roman Elizarov about 3 years
suspendCancellableCoroutine
is preferable when the underlying callback API provides the ability to cancel the ongoing operation.