Fast Barcode Scanning with ML Kit

AndroidKotlinML KitBarcode
Share on LinkedIn

Barcode scanning is the ML Kit feature I reach for most, because it's fast, works offline, handles a long list of symbologies, and — importantly — has two very different integration paths depending on how much control you want. Pick the wrong one and you either reinvent a camera UI you didn't need or fight a canned UI that doesn't fit. Here's how I decide, and the details that make a scanner feel instant instead of flaky.

Two paths: canned UI vs your own

The first decision is whether you build the scanning screen at all:

If you just need "scan a code and move on," the code scanner ships in an hour:

val options = GmsBarcodeScannerOptions.Builder()
    .setBarcodeFormats(Barcode.FORMAT_QR_CODE, Barcode.FORMAT_EAN_13)
    .enableAutoZoom()
    .build()

GmsBarcodeScanning.getClient(this, options).startScan()
    .addOnSuccessListener { barcode -> handle(barcode) }
    .addOnCanceledListener { /* user backed out */ }
    .addOnFailureListener { e -> log(e) }

enableAutoZoom() alone dramatically improves scanning small or distant codes — it's the single best flag in the canned scanner.

Restrict formats — it's the biggest speed win

Whichever path you take, tell the detector exactly which symbologies to look for. Searching for every format is slower than searching for one, and the difference is visible in live scanning latency:

val options = BarcodeScannerOptions.Builder()
    .setBarcodeFormats(Barcode.FORMAT_QR_CODE)   // only what you need
    .build()

val scanner = BarcodeScanning.getClient(options)

If your app only reads QR codes for a check-in flow, asking for QR and nothing else makes the scanner noticeably snappier. Only request the union of formats you actually support.

The custom pipeline

For a branded viewfinder with an overlay, run the scanner over CameraX frames:

@OptIn(ExperimentalGetImage::class)
private fun analyze(proxy: ImageProxy) {
    val mediaImage = proxy.image ?: run { proxy.close(); return }
    val input = InputImage.fromMediaImage(mediaImage, proxy.imageInfo.rotationDegrees)
    scanner.process(input)
        .addOnSuccessListener { barcodes ->
            barcodes.firstOrNull()?.let { onDetected(it) }
        }
        .addOnCompleteListener { proxy.close() }   // close after processing
}

Same lifecycle rules as any on-device ML Kit vision feature: pass rotationDegrees, use KEEP_ONLY_LATEST backpressure, and close the ImageProxy only when the task completes. Barcode detection is light, so it comfortably runs at higher frame rates than OCR.

Parse the payload, don't just read the string

ML Kit doesn't stop at the raw string — it classifies structured QR payloads into typed objects, which saves you from error-prone hand-parsing:

when (barcode.valueType) {
    Barcode.TYPE_URL -> open(barcode.url?.url)
    Barcode.TYPE_WIFI -> connectWifi(barcode.wifi?.ssid, barcode.wifi?.password)
    Barcode.TYPE_CONTACT_INFO -> addContact(barcode.contactInfo)
    Barcode.TYPE_GEO -> showMap(barcode.geoPoint?.lat, barcode.geoPoint?.lng)
    else -> useRaw(barcode.rawValue)
}

For product barcodes (EAN/UPC), you get the numeric string and look it up yourself. For QR, lean on the typed getters — they handle the wire encodings for Wi-Fi and vCards correctly, which are fiddly to parse by hand.

Making live scanning feel reliable

The mechanics are easy; the feel is where cheap scanners lose. What I do:

  1. Debounce success. Detection fires many times a second on the same code. Latch on the first accept, stop scanning, and give haptic + visual feedback so the user knows it worked. Don't fire your callback 20 times.
  2. Region of interest. Draw a scan rectangle and prefer codes whose bounding box falls inside it, so a poster in the background doesn't hijack the scan.
  3. Auto-zoom / tap-to-focus. Small codes at distance are the top failure. Auto-zoom (canned scanner) or a torch toggle and tap-to-focus (custom) rescue low-light and distance cases.
  4. Handle multiple codes. If several codes are in frame, decide: nearest to center, largest, or prompt the user. Silently grabbing a random one confuses people.

What I'd take away

Start by choosing your integration path: the Google code scanner when you want a working scanner today with no camera code, the raw ML Kit API when you need a custom viewfinder or continuous scanning. Either way, restrict the barcode formats to exactly what you support — it's the biggest latency win — and lean on ML Kit's typed payload parsing instead of hand-rolling QR decoders. Then invest in the feel: debounce success with clear feedback, constrain to a scan region, enable auto-zoom for distant codes, and handle the multi-code case deliberately. That's what separates a scanner people trust from one they curse at in bad lighting.

Common production mistakes

Teams get barcode scanning mlkit wrong in predictable ways:

Shipping barcode scanning mlkit on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.

Resources

Frequently asked questions

Should I use the ML Kit barcode API or the Google code scanner?

Use the Google code scanner (GmsBarcodeScanning) when you want a fully built scanning UI with zero camera code — it handles the camera, permission, and autofocus and returns a barcode. Use the ML Kit BarcodeScanner API with your own CameraX pipeline when you need a custom scanning UI, an overlay, or to scan continuously within your own screen. The code scanner is faster to ship; the raw API gives you control.

How do I make barcode scanning faster in ML Kit?

Restrict the formats you scan for with BarcodeScannerOptions.setBarcodeFormats. The detector runs faster when it isn't searching for every possible symbology, so if you only need QR codes, ask only for QR. Also feed a reasonable resolution and use KEEP_ONLY_LATEST backpressure so the scanner always works on the freshest frame.

Can ML Kit parse the data inside a barcode?

Yes. Beyond the raw string, ML Kit classifies many QR payloads into typed objects — URLs, Wi-Fi credentials, contact cards, calendar events, geo points — accessible through Barcode.valueType and the typed getters. This saves you from hand-parsing structured QR content and handles the common encodings correctly.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →