Existing 3-function callback to Kotlin Coroutines

22,428

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

Share:
22,428
Benjamin H
Author by

Benjamin H

Updated on December 31, 2021

Comments

  • Benjamin H
    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
    Benjamin H over 6 years
    I'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
    GetSwifty over 6 years
    At 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
    Benjamin H over 6 years
    The 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
    Benjamin H over 6 years
  • Bolein95
    Bolein95 almost 6 years
    how do you handle coroutine cancellation in this case?
  • Gábor
    Gábor almost 5 years
    @Bolein95: by using suspendCancellableCoroutine instead.
  • Roman  Elizarov
    Roman Elizarov about 4 years
    You can choose to represent your error with exception and do cont.resumeWithException(CameraException(error)) or represent your error with a special result type and do cont.resume(CameraErrorResult(error)).
  • Willi Mentzel
    Willi Mentzel about 4 years
    this is just perfect! I <3 coroutines! :)
  • A. Steenbergen
    A. Steenbergen about 4 years
    Note that this will crash if the callback fires more than once, e.g. in some cases when rotating the device
  • DarkNeuron
    DarkNeuron over 3 years
    Classic 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
    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 or start function. For example using the above code, lets assume nothing would happen until you run CameraManager.start(). How would chaining work in such a case? Hope my question makes sense.
  • Anigif
    Anigif about 3 years
    Wouldn't it always be preferable to use suspendCancellableCoroutine in most cases?
  • Roman  Elizarov
    Roman Elizarov about 3 years
    suspendCancellableCoroutine is preferable when the underlying callback API provides the ability to cancel the ongoing operation.