Build an Android QR Scanner with ML Kit
QR codes are routine across Australia. Shoppers encounter them at some supermarkets and parcel lockers, diners scan café menus in Sydney and Melbourne, and organisers use them for council events, school fundraisers and community sport in Brisbane and regional towns. A useful scanner should therefore feel immediate on ordinary phones, including when a venue has crowded Wi-Fi or patchy mobile coverage.
ML Kit’s Android barcode API handles QR recognition, while CameraX provides the camera preview. Recognition can remain on the phone, avoiding routine uploads of camera images. Any DigitalOcean or phpMyAdmin setup associated with this page is optional server administration, not a requirement for scanning; keep database work outside the frame-processing path.
Prepare the Android project
Use Kotlin, CameraX and View Binding, with a minimum SDK of 24 unless you must support older phones. The dependency below uses ML Kit’s bundled model, so recognition does not wait for a model download. That is useful for regional travel, busy community events and prepaid mobile plans with limited data.
Add the camera and scanning libraries to the app module:
dependencies {
implementation("androidx.camera:camera-camera2:1.4.1")
implementation("androidx.camera:camera-lifecycle:1.4.1")
implementation("androidx.camera:camera-view:1.4.1")
implementation("com.google.android.gms:play-services-mlkit-barcode-scanning:17.3.0")
}
Declare android.permission.CAMERA in the manifest, then request it at runtime through ActivityResultContracts.RequestPermission after showing a plain-language explanation. If the app also accepts image imports, mark the camera as optional and offer a picker-based fallback. Keep image analysis on the device unless the user knowingly chooses a service that requires an upload.
Connect the camera preview
Create a PreviewView in the activity layout. After camera permission is granted, obtain a ProcessCameraProvider, bind the preview to the activity lifecycle and attach an ImageAnalysis use case. STRATEGY_KEEP_ONLY_LATEST drops stale frames so the analyser does not build a queue while the camera produces images.
Configure the scanner for QR codes, then pass each ImageProxy to ML Kit. Always close the proxy after the asynchronous task finishes, or a busy screen will eventually exhaust the available image buffers.
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val scanner = BarcodeScanning.getClient(options)
class QrAnalyzer(
private val scanner: BarcodeScanner,
private val onValue: (String) -> Unit
) : ImageAnalysis.Analyzer {
private var handled: String? = null
override fun analyze(imageProxy: ImageProxy) {
val media = imageProxy.image
if (media == null) {
imageProxy.close()
return
}
val image = InputImage.fromMediaImage(
media,
imageProxy.imageInfo.rotationDegrees
)
scanner.process(image)
.addOnSuccessListener { results ->
val value = results.firstOrNull {
it.format == Barcode.FORMAT_QR_CODE
}?.rawValue
if (!value.isNullOrBlank() && value != handled) {
handled = value
onValue(value)
}
}
.addOnCompleteListener { imageProxy.close() }
}
}
Bind the preview and analysis use case with the rear camera selector, create a dedicated executor for analysis and close the scanner when the screen is destroyed. On phones without a usable rear camera, offer image import or another camera path rather than failing silently.
Decode and validate QR results
A QR payload is untrusted input. It might contain an HTTPS address, plain text, a telephone number, Wi-Fi credentials or a one-time code, so do not open or execute every string. Parse it by type, restrict supported schemes and require confirmation before navigation or a sensitive action.
Keep an allowlist of hosts for links that may open, and display the destination before following it. For unsupported payloads, show the raw text with a copy action instead. Log a payload only after removing personal, payment and authentication data; camera frames and complete QR contents should not enter analytics.
val uri = Uri.parse(rawValue)
val allowedHosts = setOf("example.com.au")
val safeWebLink =
uri.scheme.equals("https", ignoreCase = true) &&
uri.host in allowedHosts
if (safeWebLink) {
showLinkConfirmation(uri.toString())
} else {
showRawPayload(rawValue)
}
Debounce repeated values by remembering the last accepted code, while resetting it when the user dismisses the result. Give feedback through spoken output, vibration or a clear visual banner, and include accessible labels. In loud or bright Australian venues, haptic and visual confirmation is often more reliable than sound alone.
Compare scanning approaches
Choose how the recognition model is delivered and whether processing leaves the phone. ML Kit’s bundled option favours reliable first use; the unbundled option reduces the initial download; ZXing offers an open-source path; and a server-based recogniser adds network, hosting and privacy obligations.
The practical choices are:
| Approach | Recognition path | Best fit | Main trade-off |
|---|---|---|---|
| ML Kit bundled | Model ships with the app and runs on-device | Public launches and weak coverage | Larger app download |
| ML Kit unbundled | Model downloads in the background | Smaller installs or frequent model changes | The first scan may wait |
| ZXing | Open-source decoder on the device | Offline-first custom builds | More integration work |
| Server recogniser | Image is sent to a hosted service | Specialist batch workflows | Latency, cost, privacy and uptime duties |
For most Australian consumer launches, the bundled on-device option is the simplest dependable choice. A DigitalOcean Droplet can host an optional records API, and phpMyAdmin can administer its MySQL data, but neither should sit between the camera and recognition. Send only a validated identifier when a business genuinely needs scan counts, and protect the API with TLS, authentication and least-privilege database access.
Prepare a responsible Australian release
When the operator is covered by the Privacy Act 1988, the Australian Privacy Principles govern the collection and use of personal information. State the purpose before camera access, explain what is recorded, set a deletion period and avoid storing raw scans by default. If records go to hosting outside Australia, assess APP 8 and the security of that transfer. Public-sector clients in New South Wales, Victoria and South Australia may also impose state privacy requirements.
Release readiness checklist
Release checks
- Explain camera access in context and handle a denied permission.
- Complete the Google Play privacy and data-safety disclosures.
- Test permission denial, invalid codes and poor connectivity.
- Publish a support contact and a clear data-deletion process.
Scan-time checks
- Validate supported payload types before taking action.
- Confirm the destination of every external link.
- Exclude authentication codes and personal details from logs.
- Reset the duplicate-code guard when a result is dismissed.
For a local-market product, small traders, cafés, clubs and event organisers often need simple exports rather than a complex customer profile. If a scan starts a marketing message, the Spam Act 2003 requires consent and sender identification. If the app supports purchases or quotes, consumer-facing claims must also remain consistent with the Australian Consumer Law. Test the flow with a prepaid Android phone, a low-memory handset and unreliable connectivity.
Create a tester build now, scan a deliberately oversized code on more than one Android handset, and walk through permission denial and payload rejection before publishing. Complete the Play Console privacy disclosures, attach a support contact and release the version once the on-device flow and data-handling notices agree.