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:
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.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 theTask.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.