Writing an app to scan barcodes with Compose is easy, as I will show you. Without further ado, let’s get started with the code.
Request Permissions
First, let’s start with permission handling. To do this, we need to add Camera Permission and Features to the app’s manifest.
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.CAMERA"/>
A good solution for requesting permissions in Compose is provided by Google’s Accompanist libraries. Accompanist is a group of libraries that extend Jetpack Compose with some solutions for commonly needed tasks. Like permission handling, ViewPager, navigation animation and many many more.
In the modules build.gradle we need to add the dependency.
implementation "com.google.accompanist:accompanist-permissions:0.31.0"
After that we can get a PermissionState which is remembered with only one line.
rememberPermissionState(android.Manifest.permission.CAMERA)
Based on this PermissionState, we can check the status of the permission or even request it.
@ExperimentalPermissionsApi
@Composable
fun MainScreen() {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraScreen()
} else if (cameraPermissionState.status.shouldShowRationale) {
Text("Camera Permission permanently denied")
} else {
SideEffect {
cameraPermissionState.run { launchPermissionRequest() }
}
Text("No Camera Permission")
}
}
Since we can’t request the permission during composition as it would cause a crash, we need to request the permission in a SideEffect. SideEffect schedules the permission request so that the permission is requested after a successful composition.

Adding the camera and detecting barcodes
After requesting the permission, we will start with the CameraScreen. For didactic reasons, we will not use proper navigation in this example. Again, we need to add the dependencies in the build.gradle file.
implementation "androidx.camera:camera-camera2:1.2.2"
implementation "androidx.camera:camera-lifecycle:1.2.2"
implementation "androidx.camera:camera-view:1.2.2"
implementation "com.google.mlkit:barcode-scanning:17.1.0"
The CameraScreen uses the AndroidView where the camera view is displayed. In our case, we select the rear-facing camera, which is usually needed for barcode scanning. If you need specific camera functions, you can add a camera filter to the CameraSelector.
@Composable
fun CameraScreen() {
val localContext = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(localContext)
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val previewView = PreviewView(context)
val preview = Preview.Builder().build()
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
runCatching {
cameraProviderFuture.get().bindToLifecycle(
lifecycleOwner,
selector,
preview
)
}.onFailure {
Log.e("CAMERA", "Camera bind error ${it.localizedMessage}", it)
}
previewView
}
)
}
For the analyze function, we need to add an onSuccessListener which is called if there is a recognized barcode. In it we filter each detected barcode. For simplicity, the detected barcode data will be displayed with a toast message.
class BarcodeAnalyzer(private val context: Context) : ImageAnalysis.Analyzer {
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
private val scanner = BarcodeScanning.getClient(options)
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
imageProxy.image?.let { image ->
scanner.process(
InputImage.fromMediaImage(
image, imageProxy.imageInfo.rotationDegrees
)
).addOnSuccessListener { barcode ->
barcode?.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rawValue }
?.joinToString(",")
?.let { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() }
}.addOnCompleteListener {
imageProxy.close()
}
}
}
}
With the code above we can display the Camera Feed, but how can we scan barcodes? We need to add an analyzer. The analyzer scans for barcodes, in our case we try to recognize all supported formats.
.addOnSuccessListener { barcode ->
barcode?.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rawValue }
?.joinToString(",")
?.let { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() }
Customize barcode symbologies
We can change the symbologies we want to recognize by setting setBarcodeFormats. For performance reasons, only the required symbologies should be configured. The desired values can be easily added as additional parameters as follows:
setBarcodeFormats(Barcode.FORMAT_QR_CODE, Barcode.FORMAT_CODABAR)
The symbologies currently supported by ML Kit are:
public static final int FORMAT_UNKNOWN = -1;
public static final int FORMAT_ALL_FORMATS = 0;
public static final int FORMAT_CODE_128 = 1;
public static final int FORMAT_CODE_39 = 2;
public static final int FORMAT_CODE_93 = 4;
public static final int FORMAT_CODABAR = 8;
public static final int FORMAT_DATA_MATRIX = 16;
public static final int FORMAT_EAN_13 = 32;
public static final int FORMAT_EAN_8 = 64;
public static final int FORMAT_ITF = 128;
public static final int FORMAT_QR_CODE = 256;
public static final int FORMAT_UPC_A = 512;
public static final int FORMAT_UPC_E = 1024;
public static final int FORMAT_PDF417 = 2048;
public static final int FORMAT_AZTEC = 4096;

Finally, we can scan barcodes. With only 2 classes and under 100 lines of code we were able to add a camera and can use it to detect a number of barcodes, this would not be possible without Jetpack Compose.
See the complete Code on Github.