wise.potato
wise.potato

Reputation: 210

Queue multiple async calls with callbacks

I need to fetch some images from the gallery, process them (resize, compress...) and save them to a certain path. However, i need to queue the calls because older devices won't be able to process multiple images at the same time.

I am using Glide, this is the code used for processing one image:

fun processImage(context: Context, sourcePath: String, destinationPath: String, quality: Int, width: Int, height: Int, deleteOriginal: Boolean, callback: ((success: Boolean) -> Unit)) {
    val sourceFile = File(sourcePath)
    val destinationFile = File(destinationPath)

    GlideApp.with(context)
            .asBitmap()
            .load(sourceFile)
            .into(object : SimpleTarget<Bitmap>(width, height) {
                override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                    try {
                        destinationFile.writeBytes(ImageUtilities.imageToByteArray(resource, quality, Bitmap.CompressFormat.JPEG, false))
                        if (deleteOriginal) {
                            val originalFile = File(sourcePath)
                            originalFile.delete()
                        }
                        callback.invoke(true)
                    } catch (ex: Exception) {
                        callback.invoke(false)
                    }
                }
            })
}

Now i am queuing the calls manually by calling processNextImage which calls itself recursively until all the images are processed:

private fun processImages(sourceImagePaths: List<String>) {
        processNextImage(sourceImagePaths, 0)
}

private fun processNextImage(sourceImagePaths: List<String>, index: Int) {
    val imagePath = sourceImagePaths[index]
    val destination = FileUtilities.generateImagePath()
    processImage(this, imagePath, destination, 90, 1000, 1000, false) {
        processedImagePaths.add(destination)
        if (index + 1 < sourceImagePaths.count())
            processImage(sourceImagePaths, index + 1)
        else
            success()
    }
}

However I don't think this is the best way to do it and I tried to look into Kotlin coroutines but all I found were examples when the queued code is already blocking, which doesn't fit my case because Glide already handles the resizing asynchronously and returns the result in a callback onResourceReady

Any ideas for a clean way to do this?

Upvotes: 3

Views: 2082

Answers (2)

Marko Topolnik
Marko Topolnik

Reputation: 200138

As described in the official documentation, there is a simple pattern to follow if you want to turn a callback-based API into one based on suspendable functions. I'll paraphrase that description here.

Your key tool is the function from the standard library called suspendCoroutine(). Assume that you have someLongComputation function with a callback that receives a Result object:

fun someLongComputation(params: Params, callback: (Result) -> Unit)

You can convert it into a suspending function with the following straightforward code:

suspend fun someLongComputation(params: Params): Result = 
    suspendCoroutine { cont ->
        someLongComputation(params) { cont.resume(it) }
} 

Note how the type of the object passed to the original callback became simply the return value of the suspendable function.

With this you can see the magic of coroutines happen right in front of you: even though it looks exactly like a blocking call, it isn't. The coroutine will get suspended behind the scenes and resume when the return value is ready — and how it will resume is totally under your control.

Upvotes: 1

wise.potato
wise.potato

Reputation: 210

I was able to solve the issue using suspendCoroutine as suggested in Marko's comment, here is my code:

private fun processImages(sourceImagePaths: List<String>) {
    async(UI) {
        sourceImagePaths.forEach { path ->
            processNextImage(path)?.let {
                processedImagePaths.add(it)
            }
        }

        if (processedImagePaths.isEmpty()) finishWithFailure() else finishWithSuccess()
    }
}

private suspend fun processNextImage(sourceImagePath: String): String? = suspendCoroutine { cont ->
    val destination = FileUtilities.generateImagePath()
    processImage(this, sourceImagePath, destination, 90, 1000, 1000, false) { success ->
        if (success)
            cont.resume(destination)
        else
            cont.resume(null)

    }
}

The method processImages iterates over the list of paths, and calls processNextImage for each path. Since processNextImage contains a suspendCoroutine, it will block the thread until cont.resume is called, which guarantees that the next image will not be processed before the current one is done.

Upvotes: 0

Related Questions