Full Project:
QR codes and barcodes are everywhere, whether it’s scanning tickets at events, accessing product details in a supermarket, or enabling seamless payments. These compact codes have become essential tools for data sharing, authentication, and inventory management.
As an Android developer, integrating barcode and QR code scanning into your app can unlock a wide range of possibilities for your users. In this blog, we’ll explore how to build a modern scanner using Kotlin, Jetpack Compose, and Google ML Kit. By the end of this guide, you’ll have a fully functional scanner that leverages the power of machine learning and the simplicity of declarative UI development.
What we are going to cover:
- Setting up the project and adding dependencies.
- Setting up camera permission
- Setting up the Scanner
- Running the application and Scanning a QR or Bar code
. . .
1 Setting up the project and adding dependencies
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools"> | |
<uses-permission android:name="android.permission.CAMERA"/> | |
<uses-feature | |
android:name="android.hardware.camera" | |
android:required="true" /> |
Needed for the Camera Permission
To enable barcode and QR code scanning, your app needs access to the device’s camera. Start by adding the CAMERA
permission to your AndroidManifest.xml
file. This permission is required to use the camera for scanning:
<uses-permission android:name="android.permission.CAMERA" />
Additionally, if your app relies on the camera as a core feature (and won’t function properly without it), you should declare this using the <uses-feature>
tag. This ensures that your app is only installed on devices with a camera:
<uses-feature android:name="android.hardware.camera" android:required="true" />
If the camera is optional for your app, you can set android:required="false"
instead. These steps ensure your app has the necessary permissions and hardware requirements to support barcode scanning. In the sample application android:required="true"
because the camera is needed for the scanning of the Bar-code and QR-code.
libs.versions.toml
[versions] | |
cameraCore = "1.4.1" | |
cameraMlkitVision = "1.4.1" | |
camerax = "1.4.1" | |
mlkit-barcode-scanning = "17.3.0" | |
accompanistPermissions = "0.34.0" | |
[libraries] | |
camera-mlkit-vision = { group = "androidx.camera", name = "camera-mlkit-vision", version.ref = "cameraMlkitVision" } | |
camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } | |
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } | |
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } | |
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraCore" } | |
mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit-barcode-scanning" } | |
#For the permission | |
accompanistPermissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } |
Needed dependences for CameraX and MLkit
Gradle app build file:
// ML Kit Barcode Scanning | |
implementation(libs.mlkit.barcode.scanning) | |
implementation(libs.camera.mlkit.vision) | |
// CameraX dependencies for camera integration | |
implementation(libs.androidx.camera.core) | |
implementation(libs.camera.camera2) | |
implementation(libs.camera.lifecycle) | |
implementation(libs.camera.view) | |
// Accompanist Permissions for handling runtime permissions | |
implementation(libs.accompanistPermissions) |
Gradle dependences
2 Setting up camera permission:
For the permission we are going to use the accompanist permissions library. The dependency has already been setup in the previous set, so we can move straight to the implementation.
I will be making a composable for the HomeScreen. For simplicity of this article this composable will handle and host the permission state as well as the result from the Scanned QR code or Barcode.
@OptIn(ExperimentalPermissionsApi::class) // Opt-in to use experimental permissions API | |
@Composable | |
fun HomeScreen(modifier: Modifier = Modifier) { | |
// State to hold the scanned barcode value, saved across recompositions | |
var barcode by rememberSaveable { mutableStateOf<String?>("No Code Scanned") } | |
// State to manage the camera permission | |
val permissionState = rememberPermissionState( | |
Manifest.permission.CAMERA // Permission being requested | |
) | |
// State to track whether to show the rationale dialog for the permission | |
var oncancel by remember(permissionState.status.shouldShowRationale) { | |
mutableStateOf(permissionState.status.shouldShowRationale) | |
} |
Code for the permission
3 Setting up the Scanner
3.1 Setting Up the Camera with CameraX:
To implement the camera functionality (which will be needed for the scanning of the barcode) we are going to use CameraX, a modern Android library that simplifies camera operations and ensures compatibility across devices. The LifecycleCameraController
is initialized and is used to simplify camera interactions by binding the camera’s lifecycle to a LifecycleOwner (ex: an Activity or Fragment). and bound to the lifecycleOwner
(ex: an Activity ), ensuring the camera respects the app’s lifecycle (for ex: pausing when the app goes to the background). The AndroidView
composable is used to integrate the PreviewView
, which displays the live camera feed.
// Initialize the camera controller with the current context | |
val cameraController = remember { | |
LifecycleCameraController(context) | |
} | |
// AndroidView to integrate the camera preview and barcode scanning | |
AndroidView( | |
modifier = modifier.fillMaxSize(), // Make the view take up the entire screen | |
factory = { ctx -> | |
PreviewView(ctx).apply { | |
// Bind the camera controller to the lifecycle owner | |
cameraController.bindToLifecycle(lifecycleOwner) | |
// Set the camera controller for the PreviewView | |
this.controller = cameraController | |
} | |
} | |
) |
Setting up the Camera
In this block:
LifecycleCameraController
manages the camera’s lifecycle and simplifies camera operations.
AndroidView
is used to embed thePreviewView
(a traditional AndroidView
) into a Jetpack Compose UI.The
PreviewView
displays the live camera feed, and thecameraController
is bound to thelifecycleOwner
to ensure proper lifecycle handling.
3.2 Implementing Barcode Scanning with ML Kit
Configuring Barcode Scanner Options:
The first step in implementing barcode scanning is to configure the barcode formats that the scanner should detect. Google ML Kit supports a wide range of barcode formats, including QR codes, UPC codes, and more. We use the BarcodeScannerOptions.Builder()
to specify the formats we want to support.
Here’s the code:
val options = BarcodeScannerOptions.Builder() | |
.setBarcodeFormats( | |
Barcode.FORMAT_QR_CODE, // QR Codes | |
Barcode.FORMAT_CODABAR, // Codabar | |
Barcode.FORMAT_CODE_93, // Code 93 | |
Barcode.FORMAT_CODE_39, // Code 39 | |
Barcode.FORMAT_CODE_128, // Code 128 | |
Barcode.FORMAT_EAN_8, // EAN-8 | |
Barcode.FORMAT_EAN_13, // EAN-13 | |
Barcode.FORMAT_AZTEC // Aztec | |
) | |
.build() |
Barcode builder
Why this matters: By specifying the formats, you ensure the scanner only looks for relevant barcodes, improving efficiency and reducing false positives.
Customization: You can add or remove formats based on your app’s requirements.
Initializing the Barcode Scanner:
//inside your ScanCode.kts file val barcodeScanner = BarcodeScanning.getClient(options)
What this does: The
barcodeScanner
is the core component that analyzes camera frames and detects barcodes.Performance: ML Kit’s barcode scanner is optimized for real-time performance and works offline, making it ideal for mobile apps.
3.3 Setting Up Image Analyzer with CameraX:
To process camera frames, we use CameraX’s ImageAnalysis API. This API allows us to analyze each frame in real time and pass it to the barcode scanner. The
MlKitAnalyzer
is a utility provided by CameraX to integrate ML Kit with image analysis.
Here’s the code:
cameraController.setImageAnalysisAnalyzer( | |
ContextCompat.getMainExecutor(ctx), // Use the main thread for analysis | |
MlKitAnalyzer( | |
listOf(barcodeScanner), // Pass the barcode scanner | |
ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, // Use view-referenced coordinates | |
ContextCompat.getMainExecutor(ctx) // Use the main thread for results | |
) { result: MlKitAnalyzer.Result? -> | |
// Process the barcode scanning results | |
val barcodeResults = result?.getValue(barcodeScanner) | |
if (!barcodeResults.isNullOrEmpty()) { | |
// Handle detected barcodes | |
} | |
} | |
) |
Setting up image Analyzer
Key Components:
ContextCompat.getMainExecutor(ctx)
: Ensures the analysis runs on the main thread.
ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
: Aligns the barcode coordinates with the camera preview.
MlKitAnalyzer
: Bridges CameraX and ML Kit, simplifying the integration.
3.4 Processing Barcode Results:
When a barcode is detected, the MlKitAnalyzer
returns a list of Barcode
objects. Each Barcode
contains information such as the raw value, format, and bounding box. We extract this information and update the UI state.
Here’s the code:
val barcodeResults = result?.getValue(barcodeScanner) | |
if (!barcodeResults.isNullOrEmpty()) { | |
// Update the barcode state with the first detected barcode | |
barcode = barcodeResults.first().rawValue | |
// Update the state to indicate a barcode has been detected | |
qrCodeDetected = true | |
// Update the bounding rectangle of the detected barcode | |
boundingRect = barcodeResults.first().boundingBox | |
// Log the bounding box for debugging purposes | |
Log.d("Looking for Barcode ", barcodeResults.first().boundingBox.toString()) | |
} |
Processing Barcode Result
Key Components:
barcodeResults.first().rawValue
: Extracts the raw value of the detected barcode (e.g., a URL or text).
boundingRect
: Stores the bounding box of the barcode, which is used to draw a rectangle around it in the UI.
qrCodeDetected
: A boolean state that triggers further actions (e.g., invoking a callback).
What we’ve covered so far:
- Configure Barcode Formats: Specify the types of barcodes the scanner should detect.
- Initialize the Scanner: Create a
BarcodeScanning
client with the configured options. - Set Up Image Analysis: Use CameraX’s
ImageAnalysis
API to process camera frames. - Process Results: Extract the barcode value and bounding box from the detected barcode.
- Update UI State: Store the barcode value and bounding box for further processing and display.
Code Example for the Entire Barcode Scanning Logic:
// Configure barcode scanning options for supported formats | |
val options = BarcodeScannerOptions.Builder() | |
.setBarcodeFormats( | |
Barcode.FORMAT_QR_CODE, | |
Barcode.FORMAT_CODABAR, | |
Barcode.FORMAT_CODE_93, | |
Barcode.FORMAT_CODE_39, | |
Barcode.FORMAT_CODE_128, | |
Barcode.FORMAT_EAN_8, | |
Barcode.FORMAT_EAN_13, | |
Barcode.FORMAT_AZTEC | |
) | |
.build() | |
// Initialize the barcode scanner client with the configured options | |
val barcodeScanner = BarcodeScanning.getClient(options) | |
// Set up the image analysis analyzer for barcode detection | |
cameraController.setImageAnalysisAnalyzer( | |
ContextCompat.getMainExecutor(ctx), // Use the main executor | |
MlKitAnalyzer( | |
listOf(barcodeScanner), // Pass the barcode scanner | |
ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, // Use view-referenced coordinates | |
ContextCompat.getMainExecutor(ctx) // Use the main executor | |
) { result: MlKitAnalyzer.Result? -> | |
// Process the barcode scanning results | |
val barcodeResults = result?.getValue(barcodeScanner) | |
if (!barcodeResults.isNullOrEmpty()) { | |
// Update the barcode state with the first detected barcode | |
barcode = barcodeResults.first().rawValue | |
// Update the state to indicate a barcode has been detected | |
qrCodeDetected = true | |
// Update the bounding rectangle of the detected barcode | |
boundingRect = barcodeResults.first().boundingBox | |
// Log the bounding box for debugging purposes | |
Log.d("Looking for Barcode ", barcodeResults.first().boundingBox.toString()) | |
} | |
} | |
) |
Mlkit full implementation
3.5 Handling Detected Barcodes
The onQrCodeDetected
parameter is a callback function that allows the parent composable to handle the detected barcode value. This is a common pattern in Jetpack Compose for passing data or events up the UI hierarchy. When a barcode is detected, the qrCodeDetected
state is set to true
, triggering a LaunchedEffect
.
// If a QR/barcode has been detected, trigger the callback | |
if (qrCodeDetected) { | |
LaunchedEffect(Unit) { | |
// Delay for a short duration to allow recomposition | |
delay(100) // Adjust delay as needed | |
// Call the callback with the detected barcode value | |
onQrCodeDetected(barcode ?: "") | |
} | |
// Draw a rectangle around the detected barcode | |
DrawRectangle(rect = boundingRect) | |
} |
LaunchedEffect Delay for DrawRectangle to complete compose
In this block:
onQrCodeDetected
is a lambda function that takes the detected barcode value as a parameter. This allows the parent composable to handle the result (e.g., navigating to a new screen or displaying the barcode data).
LaunchedEffect
is used to perform a side effect (invoking the callback) in a controlled manner. It ensures the callback is only triggered once whenqrCodeDetected
changes totrue
.A short
delay
is added to allow the UI to recompose before invoking the callback. This prevents potential race conditions and ensures a smooth user experience.
3.6 Drawing the Barcode Bounding Box
To provide visual feedback, a rectangle is drawn around the detected barcode using Jetpack Compose’s Canvas
API. The DrawRectangle
composable converts the Android Rect
to a Compose Rect
and draws it on the screen.
Here’s the code:
@Composable | |
fun DrawRectangle(rect: Rect?) { | |
// Convert the Android Rect to a Compose Rect | |
val composeRect = rect?.toComposeRect() | |
// Draw the rectangle on a Canvas if the rect is not null | |
composeRect?.let { | |
Canvas(modifier = Modifier.fillMaxSize()) { | |
drawRect( | |
color = Color.Red, | |
topLeft = Offset(it.left, it.top), // Set the top-left position | |
size = Size(it.width, it.height), // Set the size of the rectangle | |
style = Stroke(width = 5f) // Use a stroke style with a width of 5f | |
) | |
} | |
} | |
} |
Draw Rectangle Compose
In this block:
The
Rect
object from ML Kit is converted to a Compose-compatibleRect
.The
Canvas
composable is used to draw a red rectangle around the detected barcode, providing visual feedback to the user.
LaunchedEffect
is a key part of Jetpack Compose’s side-effect handling. It ensures that the callback (onQrCodeDetected
) is only triggered once when the qrCodeDetected
state changes. The delay
ensures the UI has enough time to recompose before the callback is executed, preventing any visual glitches or inconsistencies. This approach aligns with Compose’s reactive programming model, where side effects are managed explicitly to maintain a predictable and efficient UI.
Job Offers
Complete ScanCode Implementation:
@Composable | |
fun ScanCode( | |
onQrCodeDetected: (String) -> Unit, // Callback to handle detected QR/barcode | |
modifier: Modifier = Modifier | |
) { | |
// State to hold the detected barcode value | |
var barcode by remember { mutableStateOf<String?>(null) } | |
// Get the current context and lifecycle owner for camera operations | |
val context = LocalContext.current | |
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current | |
// State to track if a QR/barcode has been detected | |
var qrCodeDetected by remember { mutableStateOf(false) } | |
// State to hold the bounding rectangle of the detected barcode | |
var boundingRect by remember { mutableStateOf<Rect?>(null) } | |
// Initialize the camera controller with the current context | |
val cameraController = remember { | |
LifecycleCameraController(context) | |
} | |
// AndroidView to integrate the camera preview and barcode scanning | |
AndroidView( | |
modifier = modifier.fillMaxSize(), // Make the view take up the entire screen | |
factory = { ctx -> | |
PreviewView(ctx).apply { | |
// Configure barcode scanning options for supported formats | |
val options = BarcodeScannerOptions.Builder() | |
.setBarcodeFormats( | |
Barcode.FORMAT_QR_CODE, | |
Barcode.FORMAT_CODABAR, | |
Barcode.FORMAT_CODE_93, | |
Barcode.FORMAT_CODE_39, | |
Barcode.FORMAT_CODE_128, | |
Barcode.FORMAT_EAN_8, | |
Barcode.FORMAT_EAN_13, | |
Barcode.FORMAT_AZTEC | |
) | |
.build() | |
// Initialize the barcode scanner client with the configured options | |
val barcodeScanner = BarcodeScanning.getClient(options) | |
// Set up the image analysis analyzer for barcode detection | |
cameraController.setImageAnalysisAnalyzer( | |
ContextCompat.getMainExecutor(ctx), // Use the main executor | |
MlKitAnalyzer( | |
listOf(barcodeScanner), // Pass the barcode scanner | |
ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, // Use view-referenced coordinates | |
ContextCompat.getMainExecutor(ctx) // Use the main executor | |
) { result: MlKitAnalyzer.Result? -> | |
// Process the barcode scanning results | |
val barcodeResults = result?.getValue(barcodeScanner) | |
if (!barcodeResults.isNullOrEmpty()) { | |
// Update the barcode state with the first detected barcode | |
barcode = barcodeResults.first().rawValue | |
// Update the state to indicate a barcode has been detected | |
qrCodeDetected = true | |
// Update the bounding rectangle of the detected barcode | |
boundingRect = barcodeResults.first().boundingBox | |
// Log the bounding box for debugging purposes | |
Log.d("Looking for Barcode ", barcodeResults.first().boundingBox.toString()) | |
} | |
} | |
) | |
// Bind the camera controller to the lifecycle owner | |
cameraController.bindToLifecycle(lifecycleOwner) | |
// Set the camera controller for the PreviewView | |
this.controller = cameraController | |
} | |
} | |
) | |
// If a QR/barcode has been detected, trigger the callback | |
if (qrCodeDetected) { | |
LaunchedEffect(Unit) { | |
// Delay for a short duration to allow recomposition | |
delay(100) // Adjust delay as needed | |
// Call the callback with the detected barcode value | |
onQrCodeDetected(barcode ?: "") | |
} | |
// Draw a rectangle around the detected barcode | |
DrawRectangle(rect = boundingRect) | |
} | |
} | |
@Composable | |
fun DrawRectangle(rect: Rect?) { | |
// Convert the Android Rect to a Compose Rect | |
val composeRect = rect?.toComposeRect() | |
// Draw the rectangle on a Canvas if the rect is not null | |
composeRect?.let { | |
Canvas(modifier = Modifier.fillMaxSize()) { | |
drawRect( | |
color = Color.Red, | |
topLeft = Offset(it.left, it.top), // Set the top-left position | |
size = Size(it.width, it.height), // Set the size of the rectangle | |
style = Stroke(width = 5f) // Use a stroke style with a width of 5f | |
) | |
} | |
} | |
} |
Complete ScanCode.kts file code
Running the application and Scanning a Qrcode or Barcode:
Run your code and you will have some this like this:
Conclusion
Building a barcode and QR code scanner in Android has never been easier, thanks to the powerful combination of Jetpack Compose, CameraX, and Google ML Kit. In this article, we’ve explored how to create a seamless scanning experience by integrating these modern tools. From setting up the camera and configuring ML Kit’s barcode detection to handling scanned results and drawing bounding boxes, we’ve covered all the essential steps to get you started.
Key highlights of this implementation include:
- CameraX for reliable and lifecycle-aware camera operations.
- ML Kit for fast and accurate barcode and QR code detection, even offline.
- Jetpack Compose for building a dynamic and responsive UI with minimal boilerplate code.
- The use of
onQrCodeDetected
andLaunchedEffect
to handle scanned results efficiently and ensure a smooth user experience.
This implementation is highly customizable and can be adapted to various use cases, such as payment systems, inventory management, or event ticketing. By following this guide, you now have a solid foundation to build upon and enhance your app with advanced features like multi-scan support, custom UI overlays, or integration with cloud services.
I hope this article has provided you with the knowledge and tools to implement barcode and QR code scanning in your Android app. Feel free to experiment, iterate, and take your app to the next level. Stay happy coding!
References:
Full Project:
This article is previously published on proandroiddev.com.