105

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()
}
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Benjamin H
  • 5,164
  • 6
  • 34
  • 42
  • 1
    Chaining callbacks works if there ale multiple callbacks *in sequence*, each providing a result or error. Here are two callbacks *in parallel*, how do you imagine chaining two callbacks at once? Which one does your sample pick? Oh, primary flow. But you still need to close it onDisconnected, how do you chain it? – Eugen Pechanec Feb 01 '18 at 00:05

3 Answers3

268

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.

Roman Elizarov
  • 27,053
  • 12
  • 64
  • 60
  • https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#wrapping-callbacks YES! Thank you!! – Benjamin H Feb 01 '18 at 16:43
  • 9
    how do you handle coroutine cancellation in this case? – Bolein95 Jun 18 '18 at 20:20
  • 4
    @Bolein95: by using `suspendCancellableCoroutine` instead. – Gábor Jun 28 '19 at 23:38
  • 2
    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))`. – Roman Elizarov Feb 28 '20 at 10:25
  • 2
    this is just perfect! I <3 coroutines! :) – Willi Mentzel Apr 10 '20 at 14:41
  • 5
    Note that this will crash if the callback fires more than once, e.g. in some cases when rotating the device – A. Steenbergen May 18 '20 at 13:35
  • @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. – DarkNeuron Nov 13 '20 at 11:19
  • Wouldn't it always be preferable to use `suspendCancellableCoroutine` in most cases? – Anigif May 10 '21 at 14:58
  • 1
    `suspendCancellableCoroutine` is preferable when the underlying callback API provides the ability to cancel the ongoing operation. – Roman Elizarov May 12 '21 at 13:53
  • 1
    Yes, it might crash with `java.lang.IllegalStateException: Already resumed` if for some reason callback is called twice, like if we keep an instance of some client which eventually calls callback method. How to workaround this? Here is the actual example: https://stackoverflow.com/questions/61217073/crash-in-android-billingclient-with-coroutines – Maxim Alov Aug 19 '22 at 17:43
  • Any idea how to unit test this baby? – Entreco Apr 13 '23 at 14:15
6

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

Community
  • 1
  • 1
ilyas ipek
  • 117
  • 2
  • 5
-1

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.

GetSwifty
  • 7,568
  • 1
  • 29
  • 46
  • 2
    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()`? – Benjamin H Jan 31 '18 at 23:39
  • 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? – GetSwifty Jan 31 '18 at 23:57
  • 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 Feb 01 '18 at 00:53
  • 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 Nov 13 '20 at 11:13
  • Looks like you missed the point here... – Renetik Jul 25 '23 at 12:59