Home > Mobile >  How to await Task completion and return a variable?
How to await Task completion and return a variable?

Time:03-24

I am stuck on an attempt to write "idiomatic" Kotlin async code. I am trying to refactor a barcode scanner as a top/ package level function.

How could I make the thread wait for the scanner.process(image) to complete, and return the list of barcodes before continuing?

The code partially shows my "closest" attempt to solve the problem.

package com.example.demo

import android.util.Log
import com.google.mlkit.vision.barcode.Barcode
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import kotlinx.coroutines.*

class BarcodeActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val scanButton = findViewById<Button>(...)
        scanButton.setOnClickListener {
          // Get Bitmap image
            runBarcode(image)
        }
    }

  // Member function, calling codes should return a list of barcodes
  // after completing 
  fun runBarcode(image: InputImage) =  runBlocking {
    Log.d("BAR", "One")
    val codes = async { scanBarcodes(image)}
    Log.d("BAR", "Three")
  }
}

// Package level function
suspend fun scanBarcodes(image: InputImage) = coroutineScope {
    val scanner = BarcodeScanning.getClient()
    val codes = async {
        scanner.process(image)
            .addOnSuccessListener { barcodes ->
                val barcodeList = mutableListOf<Barcode>()
                Log.d("BAR", "Two")
                for (barcode in barcodes) {
                    barcodeList.add(barcode)
                }
                return@addOnSuccessListener
            }
            .addOnFailureListener {
                Log.e("BAR", "Barcode scan failed")
            }
    }
    codes.await()
}

prints

D/BAR One
D/BAR Three
D/BAR Two

and the inferred return type of scanBarcodes is Task<MutableListOf<Barcode>>

In Dart/ Flutter I would write something like <T> scanBarcodes() async {} and accordingly var codes = await ... to solve the problem. I suppose I could use val codes = runBlocking{...} to block the main thread while completing. However, this async pattern is apparently strongly discouraged in Kotlin.

CodePudding user response:

  1. Don't use runBlocking for this. It will block your main thread, which freezes up the UI (user cannot scroll, click or even navigate away from your app) while it's waiting. It also puts you at risk of an ANR crash. You should launch a coroutine from your click listener so you can make this function a suspend function.

  2. To use a callback as a suspend function, often you have to convert it into something that suspends by using suspendCancellableCoroutine(). However, the Kotlin coroutines library already provides this for you in this case with the Task.await() suspend function. If it is not available to import in your project, add this dependency to your build.gradle:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.0"

The await() function returns the result of the task, or it throws an exception if the task fails, so you should wrap it in try/catch instead of using success and failure listeners.

Here's how to fix your code:

class BarcodeActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val scanButton = findViewById<Button>(...)
        scanButton.setOnClickListener {
            lifecycleScope.launch { 
                val barcodes = scanBarcodes(image)
                // do something with returned barcodes or maybe show message if list empty
            }
        }
    }

}

// Package level function
suspend fun scanBarcodes(image: InputImage): List<Barcode> {
    return try {
        BarcodeScanning.getClient()
            .process(image)
            .await()
    } catch(e: Exception) {
        Log.e("BAR", "Barcode scan failed")
        emptyList() // Returns an empty list on failure, but you might want to handle it differently, like returning null.
    }
}

CodePudding user response:

You can rewrite the fun runBarcode() as below.

fun runBarcode(image: InputImage) {
    lifecycle.coroutineScope.launch {
        Log.d("BAR", "One")
        val codes = async { scanBarcodes(image)}
        codes.await()
        Log.d("BAR", "Three")
    }
}

await() is the suspendable function this will return DeferedJob object, which has the result value which returns from suspendable function (DefferedJob()) if any. This will help you to print

D/BAR One
D/BAR Two
D/BAR Three

Hope this is what you are expecting ..

CodePudding user response:

For completeness and future googlers, I show the working functions as they are modified to fit the accepted answer from @Tenfour04.

First the BarcodeActivity member function

fun runBarcode(inputImage: InputImage) {
    message = findViewById(R.id.barcodePresenter)
    lifecycleScope.launch {
        val codes = scanBarcodes(image = inputImage)
        val rawValue = codes[0].rawValue
        message?.setText(String.format("Barcode: %s", rawValue))
    }
}

then the package level function

import kotlinx.coroutines.tasks.await

suspend fun scanBarcodes(image: InputImage):List<Barcode>  {
    return try {
        BarcodeScanning.getClient()
        .process(image)
        .addOnSuccessListener { barcodes ->
            val barcodeList = mutableListOf<Barcode>()
            for (barcode in barcodes) {
                barcodeList.add(barcode)
            }
            return@addOnSuccessListener
        }
        .await()
    } catch (e: java.lang.Exception) {
        Log.e("BAR", "Barcode scan failed")
        return emptyList()
    }

}

Perhaps @Tenfour04 does not appreciate the mix of try/catch and onSuccessListener, but it works and is coherent with my original attempt/ question.

  • Related