Existing 3-function callback to Kotlin Coroutines

All we need is an easy explanation of the problem, so here it is.

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()
}

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 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.

Method 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

Method 3

I’ve used 2 solutions for this type of thing.

1: wrap the interface in an extension

CameraDevice.openCamera(cameraId: Integer, 
                onOpenedCallback: (CameraDevice) -> (), 
          onDisconnectedCallback: (CameraDevice) ->()) {

    manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
        override fun onOpened(openedCameraDevice: CameraDevice) {
            onOpenedCallback(openedCameraDevice)
        }

        override fun onDisconnected(cameraDevice: CameraDevice) {
            onDisconnectedCallback(cameraDevice)
        }
   })
}

2: Make a simple container class with a more functional interface:

class StateCallbackWrapper(val onOpened: (CameraDevice) -> (), val onClosed: (CameraDevice) ->()): CameraDevice.StateCallback() {
    override fun onOpened(openedCameraDevice: CameraDevice) {
        onOpened(openedCameraDevice)
    }

    override fun onDisconnected(cameraDevice: CameraDevice) {
        onClosed(cameraDevice)
    }
}

Personally I would start with something like these, and then build whatever threading differences on top of that.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply