Blog Infos
Author
Published
Topics
, , , ,
Author
Published

Full Project:

https://github.com/DUMA042/BarsandQ

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 KotlinJetpack 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:

  1. Setting up the project and adding dependencies.
  2. Setting up camera permission
  3. Setting up the Scanner
  4. 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)
}
view raw HomeScreen.kt hosted with ❤ by GitHub

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
}
}
)
view raw ScanCode.kts hosted with ❤ by GitHub

Setting up the Camera

In this block:

LifecycleCameraController manages the camera’s lifecycle and simplifies camera operations.

AndroidView is used to embed the PreviewView (a traditional Android View) into a Jetpack Compose UI.

The PreviewView displays the live camera feed, and the cameraController is bound to the lifecycleOwner 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()
view raw ScanCode.kts hosted with ❤ by GitHub

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
}
}
)
view raw ScanCode.kts hosted with ❤ by GitHub

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())
}
view raw ScanCode.kts hosted with ❤ by GitHub

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:

  1. Configure Barcode Formats: Specify the types of barcodes the scanner should detect.
  2. Initialize the Scanner: Create a BarcodeScanning client with the configured options.
  3. Set Up Image Analysis: Use CameraX’s ImageAnalysis API to process camera frames.
  4. Process Results: Extract the barcode value and bounding box from the detected barcode.
  5. 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())
}
}
)
view raw ScanCode.kts hosted with ❤ by GitHub

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)
}
view raw ScanCode.kts hosted with ❤ by GitHub

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 when qrCodeDetected changes to true.

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
)
}
}
}
view raw ScanCode.kts hosted with ❤ by GitHub

Draw Rectangle Compose

In this block:

The Rect object from ML Kit is converted to a Compose-compatible Rect.

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

No results found.

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
)
}
}
}
view raw ScanCode.kts hosted with ❤ by GitHub

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:

Screen After permission has been Granted
When Button is clicked and Camera is placed on a Qrcode
After the Scanned result has been Gotten
Conclusion

Building a barcode and QR code scanner in Android has never been easier, thanks to the powerful combination of Jetpack ComposeCameraX, 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 and LaunchedEffect 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:

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu