How to Build a QR Scanner App in Jetpack Compose (2025 Step-by-Step Guide for Android Developers)
π Introduction
π§ Why This Tech Stack Works Best: Jetpack Compose, CameraX & ML Kit Together
- Jetpack Compose is Android’s modern UI toolkit — fast to write, fun to use, and no more XML clutter.
- CameraX is Google’s recommended camera library. It simplifies working with device cameras across many models and versions.
- ML Kit (Barcode Scanning) gives us free access to powerful Google-powered machine learning tools — no need to train your own models.
How to build AI Chat Bot App in Jetpack Compose?
π ️ Tools & Dependencies Required
1. ✅ Android Studio Requirements:
- Android Studio Hedgehog (2023.1.1) or later
- Compile SDK: 34 or above
- Kotlin version: 1.9.0 or later
2. π¦ Gradle Dependencies
// CameraX
implementation ("androidx.camera:camera-core:1.4.2")
implementation ("androidx.camera:camera-camera2:1.4.2")
implementation ("androidx.camera:camera-lifecycle:1.4.2")
implementation ("androidx.camera:camera-view:1.4.2")
// ML Kit for barcode scanning
implementation ("com.google.mlkit:barcode-scanning:17.3.0")3. Don’t forget to add required INTERNET permissions in your AndroidManifest.xml:<uses-permission android:name="android.permission.CAMERA" />4. MainActivity
package com.example.scanneranalyzer
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.scanneranalyzer.ui.theme.ScannerAnalyzerTheme
class MainActivity : ComponentActivity() {
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
hasCameraPermission = granted
}
private var hasCameraPermission by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA)
enableEdgeToEdge()
setContent {
ScannerAnalyzerTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
if (hasCameraPermission) {
QRScannerScreen()
} else {
Text("Camera permission is required")
}
}
}
}
}5. π·Show the Camera Feed with CameraX
@Composable
fun CameraPreview(
context: Context,
lifecycleOwner: LifecycleOwner,
modifier: Modifier = Modifier,
onQrCodeScanned: (String) -> Unit
) {
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val previewView = remember { PreviewView(context) }
// Compose wrapper to show Camera Preview
AndroidView(
modifier = modifier,
factory = { previewView }
)
// Camera setup logic in Composable scope
LaunchedEffect(true) {
try {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().apply {
surfaceProvider = previewView.surfaceProvider
}
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.apply {
setAnalyzer(
ContextCompat.getMainExecutor(context),
QRScannerAnalyzer(onQrCodeScanned)
)
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
} catch (e: Exception) {
Log.e("CameraPreview", "Camera binding failed", e)
}
}
}6. π Detect QR Codes with MLKit Barcode Scanner
class QRScannerAnalyzer(
private val onQrScanned: (String) -> Unit
) : ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient()
private var lastScanned = ""
@OptIn(ExperimentalGetImage::class)
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: return
if (imageProxy.format != ImageFormat.YUV_420_888) {
imageProxy.close()
return
}
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcode.rawValue?.let {
if (it != lastScanned) {
lastScanned = it
onQrScanned(it)
}
}
}
}
.addOnFailureListener { Log.e("QRScanner", "Scanning error", it) }
.addOnCompleteListener { imageProxy.close() }
}
}7: πΌ️Put Everything Together – The UI
@Composable
fun QRScannerScreen() {
val context = LocalContext.current
var lastScannedTime by remember { mutableStateOf(0L) }
var scannedText by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().systemBarsPadding()) {
Text("Coding Bihar QR Scanner", style = MaterialTheme.typography.headlineMedium)
Box(modifier = Modifier.fillMaxSize()) {
CameraPreview(
context = context,
lifecycleOwner = LocalLifecycleOwner.current,
modifier = Modifier.fillMaxSize(),
onQrCodeScanned = { qr ->
val currentTime = System.currentTimeMillis()
// Allow action if it's a new QR OR if last scan was >2s ago
if (qr != scannedText || (currentTime - lastScannedTime) > 2000) {
scannedText = qr
lastScannedTime = currentTime
Toast.makeText(context, "Scanned: $qr", Toast.LENGTH_SHORT).show()
when {
qr.startsWith("http://") || qr.startsWith("https://") -> {
val intent = Intent(Intent.ACTION_VIEW, qr.toUri())
context.startActivity(intent)
}
qr.startsWith("mailto:") -> {
val intent = Intent(Intent.ACTION_SENDTO, qr.toUri())
context.startActivity(intent)
}
qr.matches(Regex("^\\+?[0-9]{7,15}$")) -> {
val intent = Intent(Intent.ACTION_DIAL, "tel:$qr".toUri())
context.startActivity(intent)
}
qr.startsWith("smsto:") || qr.startsWith("sms:") -> {
val intent = Intent(Intent.ACTION_SENDTO, qr.toUri())
context.startActivity(intent)
}
else -> {
Toast.makeText(context, "Text: $qr", Toast.LENGTH_LONG).show()
}
}
}
}
)
if (scannedText.isNotEmpty()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Scanned: $scannedText",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
}



