Android QR Code Generator and Scanner with Kotlin and Jetpack Compose — Step-by-Step Guide

- Andrés Cruz - ES En español

Video thumbnail

Continuing with image generation—as we saw earlier when working with the Canvas in Android—, in this entry we will see how to generate a QR code directly from an Android application using Jetpack Compose and the ZXing library.

What is a QR code?

Before diving deep into the implementation, it is important to understand what a QR code is and how its structure works. This will help us make better design decisions when integrating it into our apps.

A QR (Quick Response) code is simply a system to store information in the form of a two-dimensional barcode. Unlike traditional barcodes—which can only be read horizontally—a QR encodes data both horizontally and vertically, allowing it to store significantly more information in the same space.

Example QR code
Example of a QR code.

QR codes are extremely versatile: they serve to manage inventory, share URLs, contact details, access credentials, and much more. Nowadays they are an almost essential element in any medium or large-scale application, both due to their ease of use and their widespread adoption among users.

What is a QR code for and how can we leverage it in our Android apps?

QR codes are used in virtually every field: from labeling physical products such as food, medicines, and electronic devices, to digital marketing, payment systems, and linking accounts across platforms. In an Android app, you can integrate a QR reader to allow users to scan any type of code without having to leave your application.

A concrete use case: imagine a pharmacy app where the customer scans the QR code of a medication and the system—with the user's profile already loaded—automatically indicates whether that drug is compatible with their current treatments, without the user having to enter any data manually.

How to generate a QR code?

On the internet you will find multiple ways to generate QR codes: there are libraries for PHP, Java, Python, Kotlin, and many other languages. On this blog you will find articles on how to generate QRs with different technologies; today we will focus on programmatic generation from Android with ZXing.

Generating QR codes in Android with Jetpack Compose

We are going to learn how to generate QR codes directly from an Android application with Jetpack Compose. This is especially useful today, as almost any complex app requires this type of mechanism: from platforms like Netflix (to link devices) to payment gateways or authentication systems. QRs are the natural evolution of barcodes, and integrating them in Android is simpler than it seems.

1. Configuring the dependency

To generate QRs we will use the ZXing (Zebra Crossing) library, one of the most well-known in the Android ecosystem. It allows both scanning and generating QR codes and barcodes.

Open your app/build.gradle.kts file and add the following dependency:

implementation("com.google.zxing:core:3.5.4")

As always, verify that you are using the latest version: if you hover over the dependency in Android Studio, the IDE will suggest if there is a newer version available. Once added, click Sync Now and wait for the synchronization to finish to avoid import errors.

Function logic: from data to Bitmap

We will create a top-level function (outside of any class or @Composable) so that it is accessible from anywhere in the project. This function will receive two parameters: the content we want to embed in the QR (text, URL, or number) and the size in pixels of the resulting image.

The concept of BitMatrix and Bitmap

The function will return a Bitmap object. A Bitmap is a matrix representation of an image, where each position (x, y) stores color information in pixel format. The QR generation process follows three steps:

  1. Encoding: We use QRCodeWriter().encode() to convert the data into a BitMatrix, which is the logical representation of the QR (a grid of turned-on and turned-off points).
  2. Dimensions: We get the width and height of the BitMatrix to create a Bitmap of the exact size.
  3. Creating the Bitmap: We instantiate the object using createBitmap(). You will see examples online using the old API Bitmap.createBitmap, but the modern way uses Kotlin's extension function with named parameters, specifying the color format: Bitmap.Config.RGB_565 (less memory) or ARGB_8888 (with alpha channel, ideal if you need transparency).

Rendering the image in Kotlin

To build the visual image, we loop through the BitMatrix pixel by pixel. Since a QR is traditionally black and white, the logic is simple:

  • If at position (x, y) the BitMatrix has active data, we paint that pixel with Color.BLACK.
  • If not, we paint it with Color.WHITE.
fun generarQR(datos: String, size: Int = 512): Bitmap? {
    return try {
        val writer = QRCodeWriter()
        // Creates the point matrix (BitMatrix)
        val bitMatrix = writer.encode(datos, BarcodeFormat.QR_CODE, size, size)
        val width = bitMatrix.width
        val height = bitMatrix.height
//      val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
        val bitmap = createBitmap(width, height, Bitmap.Config.RGB_565)

        // We traverse the matrix to paint pixel by pixel
        for (x in 0 until width) {
            for (y in 0 until height) {
                 bitmap[x, y] = if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE
                // bitmap.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
            }
        }
        bitmap
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

The function is wrapped in a try/catch block: if encoding fails (empty data, invalid size, etc.), we return null instead of crashing the app, allowing us to handle the error gracefully in the UI.

Consumption in the interface with Jetpack Compose

To display the generated QR in our interface, we will create a @Composable that follows these steps:

  1. Usage of remember: We call generarQR() inside remember(textoParaQR). This is critical so that the QR is not regenerated on every UI recomposition—which would be very expensive performance-wise—. By passing textoParaQR as a key, the QR will only regenerate when that value changes.
  2. Conversion to ImageBitmap: Compose's Image composable expects an ImageBitmap, not an android.graphics.Bitmap. For conversion we use the extension .asImageBitmap().
  3. User interface: We add the Image component with its accessibility description, a Spacer for visual separation, and a descriptive Text.
@Composable
fun PantallaQR(textoParaQR: String) {
    // We generate the bitmap and remember it to avoid unnecessary regenerations
    val qrBitmap = remember(textoParaQR) {
        generarQR(textoParaQR)
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        if (qrBitmap != null) {
            Image(
                bitmap = qrBitmap.asImageBitmap(),
                contentDescription = "QR Code for $textoParaQR",
                modifier = Modifier.size(250.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(text = "Scan to view content", style = MaterialTheme.typography.bodyMedium)
        } else {
            Text("Error generating QR")
        }
    }
}

When running the application, we will get a QR code rendered directly on screen:

Generated QR code of desarrollolibre.net
Example of generated QR to test the scanner.

Creating a QR code reader with Jetpack Compose

To build a QR scanner with Jetpack Compose, we will combine CameraX for camera access with ML Kit Barcode Scanning for frame analysis. This is the modern solution recommended by Google for any new Android app.

Advantages of Jetpack Compose for a QR reader:

  • Declarative: You define the UI once and Compose manages state changes automatically, reducing manual reactive code.
  • Less code: Significantly less code is required compared to the traditional XML + Activities approach.
  • Reusability: @Composable components are easy to combine and reuse across different parts of the app.
  • Native Kotlin compatibility: Compose was designed from scratch for Kotlin, leveraging coroutines, lambdas, and extension functions.
  • Integration with modern libraries: Integrates seamlessly with CameraX and ML Kit for an up-to-date development experience with active support from Google.

1. Dependencies (module build.gradle):

// Jetpack Compose
implementation platform('androidx.compose:compose-bom:2024.04.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2' // Or the latest version

// CameraX
implementation 'androidx.camera:camera-core:1.3.2' // Or the latest version
implementation 'androidx.camera:camera-camera2:1.3.2'
implementation 'androidx.camera:camera-lifecycle:1.3.2'
implementation 'androidx.camera:camera-view:1.3.2' // For CameraPreview Composable

// ML Kit Barcode Scanning
implementation 'com.google.mlkit:barcode-scanning:17.2.0' // Or the latest version
// For models with Google Play Services (faster/more precise recognition)
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0'

2. Permissions (AndroidManifest.xml):

Just like in the legacy approach, declaring camera permission is mandatory. Runtime handling can be done with Accompanist Permissions' rememberPermissionState or directly with AndroidX's rememberLauncherForActivityResult, as you will see in the following example.

<uses-permission android:name="android.permission.CAMERA" />      

3. Composable Structure (complete example):

Below we show you how to structure a @Composable to display the camera preview and integrate QR code analysis. The complex logic is abstracted into the auxiliary class BarcodeAnalyzer, keeping the main Composable clean and focused on the UI.

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import java.util.concurrent.Executors

// Main composable for QR reader
@Composable
fun QRCodeScannerScreen() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    var hasCameraPermission by remember { mutableStateOf(false) }
    var qrCodeResult by remember { mutableStateOf("Scanning...") }

    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { isGranted ->
            hasCameraPermission = isGranted
            if (!isGranted) {
                qrCodeResult = "Camera permission denied."
            }
        }
    )

    LaunchedEffect(Unit) {
        when {
            ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
                hasCameraPermission = true
            }
            else -> {
                permissionLauncher.launch(Manifest.permission.CAMERA)
            }
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        if (hasCameraPermission) {
            CameraPreview(onQrCodeDetected = { result ->
                qrCodeResult = result
            })
            Spacer(Modifier.height(16.dp))
            Text(text = "Result: $qrCodeResult", modifier = Modifier.padding(16.dp))
        } else {
            Text("Camera permission is needed to scan QR.", modifier = Modifier.padding(16.dp))
            Button(onClick = { permissionLauncher.launch(Manifest.permission.CAMERA) }) {
                Text("Grant Permission")
            }
        }
    }
}

@Composable
fun CameraPreview(onQrCodeDetected: (String) -> Unit) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraExecutor = remember { Executors.newSingleThreadExecutor() }

    AndroidView(
        modifier = Modifier
            .fillMaxWidth()
            .weight(1f),
        factory = { ctx ->
            val previewView = PreviewView(ctx)
            val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)

            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()
                val preview = Preview.Builder().build().also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

                val imageAnalyzer = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_LATEST)
                    .build()
                    .also {
                        it.setAnalyzer(cameraExecutor, BarcodeAnalyzer(onQrCodeDetected))
                    }

                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector,
                        preview,
                        imageAnalyzer
                    )
                } catch (exc: Exception) {
                    // Handle camera initialization errors
                }
            }, ContextCompat.getMainExecutor(ctx))
            previewView
        }
    )
    DisposableEffect(Unit) {
        onDispose {
            cameraExecutor.shutdown()
        }
    }
}

// Class to analyze camera frames
class BarcodeAnalyzer(private val onQrCodeDetected: (String) -> Unit) : ImageAnalysis.Analyzer {
    private val scanner = BarcodeScanning.getClient(
        BarcodeScannerOptions.Builder()
            .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
            .build()
    )

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            scanner.process(image)
                .addOnSuccessListener { barcodes ->
                    if (barcodes.isNotEmpty()) {
                        barcodes.firstOrNull()?.rawValue?.let { rawValue ->
                            onQrCodeDetected(rawValue)
                        }
                    }
                }
                .addOnCompleteListener {
                    imageProxy.close() // Important: close ImageProxy to release buffer
                }
        } else {
            imageProxy.close()
        }
    }
}

Let's analyze the responsibilities of each component:

  • QRCodeScannerScreen: the main @Composable. Manages the state of permissions and the result. Requests permission with LaunchedEffect upon mounting.
  • CameraPreview: wraps an AndroidView to integrate CameraX's PreviewView, as there is no native Composable for camera preview yet.
  • BarcodeAnalyzer: implements ImageAnalysis.Analyzer and uses ML Kit to process each frame. It is essential to call imageProxy.close() in addOnCompleteListener to release the buffer and not block the camera pipeline.
  • The result is returned to the main Composable via the onQrCodeDetected callback, maintaining unidirectional data flow.

This modern approach considerably simplifies UI configuration and improves code readability, leveraging Jetpack Compose's declarative paradigm along with ML Kit and CameraX.

Legacy Approach with XML (for historical reference)

In this section we will see how to implement a QR reader using the classic approach based on XML and Activities. Although it is no longer the recommended method for new projects, it is useful for understanding the evolution of Android development or maintaining apps with legacy code.

Developing a QR reader with Play Services library

Google makes a large number of APIs available to developers through its Play Services libraries. For common tasks such as reading a QR code, it is not necessary to resort to third-party libraries: Google's own library does all the heavy lifting for us.

Update note: The library com.google.android.gms:play-services-vision has been deprecated. For new implementations, Google recommends using ML Kit Barcode Scanning (com.google.mlkit:barcode-scanning or com.google.android.gms:play-services-mlkit-barcode-scanning) in combination with CameraX for camera integration.

Creating a custom QR reader in Android Studio is quite simple: we only need to include the external library corresponding to Google Play services and configure a few components.

QR Scanner: how to decode a QR code?

With the help of a mobile phone we can retrieve the information stored in a QR simply by pointing the camera at it. That is exactly the idea behind what we will implement in this section.

Developing our own QR code reader in Android

First we must add the necessary dependency in our build.gradle file.

Adding the dependency in the build.gradle file

We open the module-level build.gradle file and add the following dependency:

implementation 'com.google.android.gms:play-services:15.0.2'

Update note: The play-services dependency is too broad and includes APIs you probably don't need. It is better to use only the required modules. Furthermore, play-services-vision is deprecated. The recommended modern option is ML Kit Barcode Scanning with CameraX.

If we are only interested in the vision API to read QRs, we can use the more specific dependency in our build.gradle:

implementation 'com.google.android.gms:play-services-vision:15.0.2'

Versions are updated frequently. You can always check the latest available version in the official documentation:

Set Up Google Play Services

Update note: Using version ranges like 15.+ is not a recommended practice in production, as it can introduce unexpected incompatibilities. Always specify an exact version.

If you wanted the dependency to update automatically to sub-versions of 15.x: implementation 'com.google.android.gms:play-services-vision:15.+'

Configuring the rest of the application for the QR reader

The manifest file to handle permissions

In our AndroidManifest.xml we declare camera usage:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.desarrollolibre.qr.qr">
    <uses-permission android:name="android.permission.CAMERA" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

The view of our QR code reader

With the dependency configured, we can go to our Activity or Fragment to create the necessary objects and configurations. But first, let's define the layout with the camera view:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_gravity="center_horizontal"
        android:padding="8dp">
        <SurfaceView
            android:id="@+id/camera_view"
            android:layout_width="640px"
            android:layout_height="480px"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true" />
    </LinearLayout>
</LinearLayout>

Configuring the detector and camera in our Activity

In our Activity or Fragment we create an object of type BarcodeDetector. This class allows recognizing barcodes and QRs through the camera, returning a SparseArray with the decoded data. Next we also create the CameraSource:

        // I create the QR detector
        barcodeDetector =
                new BarcodeDetector.Builder(getContext())
                        .setBarcodeFormats(Barcode.QR_CODE)
                        .build();
        // I create the camera source
        cameraSource = new CameraSource
                .Builder(getContext(), barcodeDetector)
                .setRequestedPreviewSize(640, 480)
                .build();

The CameraSource object to obtain frames from the camera

We reference the SurfaceView from our layout; this component reflects on screen what the device's camera is capturing:

cameraView = (SurfaceView) v.findViewById(R.id.camera_view);

Next, we register the lifecycle listener of the SurfaceHolder to start and stop the camera at the right time:

        // Camera lifecycle listener
        cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                if (ContextCompat.checkSelfPermission(getContext(), android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
                    try {
                        cameraSource.start(cameraView.getHolder());
                    } catch (IOException ie) {
                        Log.e("CAMERA SOURCE", ie.getMessage());
                    }
                } else {
                    Toast.makeText(getContext(), getResources().getString(R.string.error_camara), Toast.LENGTH_SHORT).show();
                }
            }
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                cameraSource.stop();
            }
        });

A key point: the camera must be started with cameraSource.start(cameraView.getHolder()) inside the surfaceCreated callback, that is, once the surface has already been drawn. Trying to start it earlier will cause an error.

Finally, we prepare the event that will return the result of the detected QR. When barcodes.size() is greater than zero, it means that at least one code has been read:

        // I prepare the QR detector
        barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
            @Override
            public void release() {
            }
            @Override
            public void receiveDetections(Detector.Detections<Barcode> detections) {
                final SparseArray<Barcode> barcodes = detections.getDetectedItems();
                if (barcodes.size() != 0) {
                    barcodes.valueAt(0).displayValue.toString();
                    // do something with the obtained value
                }
                barcodeDetector.release();
            }
        });

With barcodes.valueAt(0).displayValue.toString() we get the data returned by the QR: it can be plain text, a number, or a URL, depending on how the code was generated.

Permissions in Android 6 and higher

Update note: The ActivityCompat and AppCompatActivity classes from the original code belong to legacy support libraries (android.support.*). In modern projects, they have been replaced by AndroidX (androidx.core.app.ActivityCompat and androidx.appcompat.app.AppCompatActivity). Permission handling follows a similar pattern, but it is always advisable to consult the latest AndroidX documentation.

Starting with Android 6 (API 23), Google introduced a new runtime permissions scheme: sensitive permissions—such as camera access—must be explicitly requested from the user, not just declared in the AndroidManifest.

Therefore, in addition to the manifest declaration, we must request the permission from our Activity's code. We add this logic when constructing the SurfaceHolder:

// Camera lifecycle listener
cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
   @Override
   public void surfaceCreated(SurfaceHolder holder) {
       if (ActivityCompat.checkSelfPermission(QRActiviy.this, Manifest.permission.CAMERA)
               != PackageManager.PERMISSION_GRANTED) {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
               if (shouldShowRequestPermissionRationale(
                       Manifest.permission.CAMERA)) ;
               requestPermissions(new String[]{Manifest.permission.CAMERA},
                       MY_PERMISSIONS_REQUEST_CAMERA);
           }
           return;
       } else {
           try {
               cameraSource.start(cameraView.getHolder());
           } catch (IOException ie) {
               Log.e("CAMERA SOURCE", ie.getMessage());
           }
       }
   }
   @Override
   public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
   }
   @Override
   public void surfaceDestroyed(SurfaceHolder holder) {
       cameraSource.stop();
   }
});

We check if the permission has already been granted using checkSelfPermission, and if not, we display the native dialog with shouldShowRequestPermissionRationale followed by requestPermissions.

The complete Activity code would look like this:

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.ActivityCompat; // Use androidx.core.app.ActivityCompat
import android.support.v7.app.AppCompatActivity; // Use androidx.appcompat.app.AppCompatActivity
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.webkit.URLUtil;
import com.google.android.gms.vision.CameraSource; // Deprecated, use ML Kit Barcode Scanning
import com.google.android.gms.vision.Detector; // Deprecated, use ML Kit Barcode Scanning
import com.google.android.gms.vision.barcode.Barcode; // Deprecated, use ML Kit Barcode Scanning
import com.google.android.gms.vision.barcode.BarcodeDetector; // Deprecated, use ML Kit Barcode Scanning
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
    private CameraSource cameraSource;
    private SurfaceView cameraView;
    private final int MY_PERMISSIONS_REQUEST_CAMERA = 1;
    private String token = "";
    private String tokenanterior = "";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        cameraView = (SurfaceView) findViewById(R.id.camera_view);
        initQR();
    }
    public void initQR() {
        BarcodeDetector barcodeDetector =
                new BarcodeDetector.Builder(this)
                        .setBarcodeFormats(Barcode.ALL_FORMATS)
                        .build();
        cameraSource = new CameraSource
                .Builder(this, barcodeDetector)
                .setRequestedPreviewSize(1600, 1024)
                .setAutoFocusEnabled(true) // Recommended for more accurate scanning
                .build();
        cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
                        != PackageManager.PERMISSION_GRANTED) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (shouldShowRequestPermissionRationale(
                                Manifest.permission.CAMERA)) ;
                        requestPermissions(new String[]{Manifest.permission.CAMERA},
                                MY_PERMISSIONS_REQUEST_CAMERA);
                    }
                    return;
                } else {
                    try {
                        cameraSource.start(cameraView.getHolder());
                    } catch (IOException ie) {
                        Log.e("CAMERA SOURCE", ie.getMessage());
                    }
                }
            }
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                cameraSource.stop();
            }
        });
        barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
            @Override
            public void release() {
            }
            @Override
            public void receiveDetections(Detector.Detections<Barcode> detections) {
                final SparseArray<Barcode> barcodes = detections.getDetectedItems();
                if (barcodes.size() > 0) {
                    token = barcodes.valueAt(0).displayValue.toString();
                    if (!token.equals(tokenanterior)) {
                        tokenanterior = token;
                        Log.i("token", token);
                        if (URLUtil.isValidUrl(token)) {
                            Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(token));
                            startActivity(browserIntent);
                        } else {
                            Intent shareIntent = new Intent();
                            shareIntent.setAction(Intent.ACTION_SEND);
                            shareIntent.putExtra(Intent.EXTRA_TEXT, token);
                            shareIntent.setType("text/plain");
                            startActivity(shareIntent);
                        }
                        new Thread(new Runnable() {
                            public void run() {
                                try {
                                    synchronized (this) {
                                        wait(5000);
                                        tokenanterior = "";
                                    }
                                } catch (InterruptedException e) {
                                    Log.e("Error", "Waiting didn't work!!");
                                    e.printStackTrace();
                                }
                            }
                        }).start();
                    }
                }
            }
        });
    }
}

QR Code Reader in Kotlin (legacy version)

The same implementation, but in Kotlin. Note the idiomatic changes: the use of val and var, the object : syntax for anonymous implementations, the safe call with !! on nullables, and the absence of semicolons:

    private var cameraSource: CameraSource? = null
    private var cameraView: SurfaceView? = null
    private val MY_PERMISSIONS_REQUEST_CAMERA = 1
    private var token = ""
    private var tokenanterior = ""
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        cameraView = findViewById<View>(R.id.camera_view)
        initQR()
    }
    fun initQR() {
        val barcodeDetector = BarcodeDetector.Builder(this)
                .setBarcodeFormats(Barcode.ALL_FORMATS)
                .build()
        cameraSource = CameraSource.Builder(this, barcodeDetector)
                .setRequestedPreviewSize(1600, 1024)
                .setAutoFocusEnabled(true)
                .build()
        cameraView!!.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {
                if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA))
                        ;
                        requestPermissions(arrayOf(Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
                    }
                    return
                } else {
                    try {
                        cameraSource!!.start(cameraView!!.holder)
                    } catch (ie: IOException) {
                        Log.e("CAMERA SOURCE", ie.message)
                    }
                }
            }
            override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
            override fun surfaceDestroyed(holder: SurfaceHolder) {
                cameraSource!!.stop()
            }
        })
        barcodeDetector.setProcessor(object : Detector.Processor<Barcode> {
            override fun release() {}
            override fun receiveDetections(detections: Detector.Detections<Barcode>) {
                val barcodes = detections.getDetectedItems()
                if (barcodes.size() > 0) {
                    token = barcodes.valueAt(0).displayValue.toString()
                    if (token != tokenanterior) {
                        tokenanterior = token
                        Log.i("token", token)
                        if (URLUtil.isValidUrl(token)) {
                            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(token))
                            startActivity(browserIntent)
                        } else {
                            val shareIntent = Intent()
                            shareIntent.action = Intent.ACTION_SEND
                            shareIntent.putExtra(Intent.EXTRA_TEXT, token)
                            shareIntent.type = "text/plain"
                            startActivity(shareIntent)
                        }
                        Thread(object : Runnable {
                            override fun run() {
                                try {
                                    synchronized(this) {
                                        wait(5000)
                                        tokenanterior = ""
                                    }
                                } catch (e: InterruptedException) {
                                    Log.e("Error", "Waiting didn't work!!")
                                    e.printStackTrace()
                                }
                            }
                        }).start()
                    }
                }
            }
        })
    }

In the previous code, you can see the complete detection flow: we retrieve the QR value with barcodes.valueAt(0).displayValue.toString(), validate it to avoid processing the same code consecutively (thanks to the tokenanterior mechanism and the Thread with wait(5000)), and finally, with URLUtil.isValidUrl(token), we determine whether the content is a URL—in which case we open the browser—or plain text, which we share with other apps using an Intent.ACTION_SEND.

Upon running the application, we will get a screen like the following:

Android App with QR scanner running in Android Studio
Screenshot of the application running with the QR reader (legacy approach).

Remember that once you obtain the code, you can do whatever you need with it: send it to your backend for validation, display it in a dialog, or open the browser. If you are interested in generating QR codes from the server side, you can check out the article on how to create QR codes in CodeIgniter.

You can use the following QR code to test your scanner; point the camera and it should return the URL of this blog:

desarrollolibre.net QR code to test the Android scanner
Test QR code — scan it to verify that the reader is working properly.

An important detail: this exact same code works to read barcodes in addition to QR codes. The core of the reader is identical; by simply adjusting the parameter in setBarcodeFormats(), you can support EAN-13, UPC-A, Code 128, and many other formats. With that, you can build a fairly powerful app that functions as a universal code scanner.

Next step: learn how to use Room Database (SQLite) in Android Studio with Kotlin.

Create your own QR code generator and reader for Android using Kotlin, ZXing, CameraX, and ML Kit Barcode Scanning with Jetpack Compose. Full code explained for Android Studio.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.