Introduction
In this article, we will learn how to build a QR code scanner app using ML Kit and the new Camera2 APIs. Of course, to decode a bitmap, we use the ZXing library just to make sure that the scope of this article remains simple for users to understand.
This article uses Jetpack Compose for UI, along with Kotlin. Only an image utility class is written in Java to manipulate bitmaps.
Let's understand the process one by one.
Step 1. Build.gradle
We need to update our gradle file first, though we need ML kit, Camera2 dependencies, and of course Zxing(very popular among Android devs.) We can use Zxing alone, but ML Kit is simple to implement and very fast. Don't forget it is Kotlin-friendly.
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.barcode.scanning)
implementation(libs.zxing.android.embedded)
Or
//Camera view creation and scanning
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.mlkit:barcode-scanning:17.3.0"
Step 2. Prepare a UI
Prepare a UI to be clicked on or where the result is to be shown.
package com.example.myapplication
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import com.example.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding),
this
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier,context: Context) {
var isButtonClick by remember {
mutableStateOf(false)
}
var result by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "QR code detection system", modifier = Modifier.padding(16.dp), fontSize = 30.sp)
Spacer(modifier = Modifier.height(40.dp))
Text(
text = "Result = "+result.ifBlank { "Click the button to start scanning" },
modifier = Modifier.padding(16.dp)
)
Spacer(modifier = Modifier.height(40.dp))
Button(
onClick = {
isButtonClick = true
},
modifier = modifier.fillMaxWidth().padding(20.dp)
) {
Text(text = "Start Scan", modifier = Modifier.padding(16.dp))
}
}
if(isButtonClick){
StartScan(onScanOpened = {
isButtonClick = false
if (it.isNotBlank()) {
result = it
}
})
}
}
@Composable
fun StartScan(onScanOpened: (String) -> Unit = {}) {
val context = LocalContext.current
var isPermissionGranted by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}
var isLaunching by remember { mutableStateOf(false) }
val scanLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val scannedData = result.data?.getStringExtra("SCAN_RESULT") ?: ""
onScanOpened(scannedData)
} else {
onScanOpened("")
}
isLaunching = false
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
isPermissionGranted = granted
if (granted && !isLaunching) {
isLaunching = true
launchScanActivity(context, scanLauncher)
}
}
LaunchedEffect(Unit) {
// Request permission only if not granted yet
if (!isPermissionGranted) {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
// Launch scan activity once permission is granted
if (isPermissionGranted && !isLaunching) {
isLaunching = true
launchScanActivity(context, scanLauncher)
}
}
}
private fun launchScanActivity(
context: Context,
scanLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>
) {
val intent = Intent(context, ScanActivity::class.java)
scanLauncher.launch(intent)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyApplicationTheme {
}
}

Join the conversation! Your thoughts help the community grow.