113

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()
}
1
  • 1
    Chaining callbacks works if there ale multiple callbacks in sequence, each providing a result or error. <s>Here are two callbacks in parallel, how do you imagine chaining two callbacks at once? Which one does your sample pick?</s> Oh, primary flow. But you still need to close it onDisconnected, how do you chain it? Commented Feb 1, 2018 at 0:05

3 Answers 3

288

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.

Sign up to request clarification or add additional context in comments.

11 Comments

how do you handle coroutine cancellation in this case?
@Bolein95: by using suspendCancellableCoroutine instead.
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)).
this is just perfect! I <3 coroutines! :)
Note that this will crash if the callback fires more than once, e.g. in some cases when rotating the device
|
8

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

Comments

-2

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.

5 Comments

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()?
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?
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.
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.
Looks like you missed the point here...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.