Content Index
- What is a QR code?
- What is a QR code for and how can we leverage it in our Android apps?
- How to generate a QR code?
- Generating QR codes in Android with Jetpack Compose
- 1. Configuring the dependency
- Function logic: from data to Bitmap
- The concept of BitMatrix and Bitmap
- Rendering the image in Kotlin
- Consumption in the interface with Jetpack Compose
- Creating a QR code reader with Jetpack Compose
- Advantages of Jetpack Compose for a QR reader:
- 1. Dependencies (module build.gradle):
- 2. Permissions (AndroidManifest.xml):
- 3. Composable Structure (complete example):
- Legacy Approach with XML (for historical reference)
- Developing a QR reader with Play Services library
- QR Scanner: how to decode a QR code?
- Developing our own QR code reader in Android
- Adding the dependency in the build.gradle file
- Configuring the rest of the application for the QR reader
- The manifest file to handle permissions
- The view of our QR code reader
- Configuring the detector and camera in our Activity
- The CameraSource object to obtain frames from the camera
- Permissions in Android 6 and higher
- QR Code Reader in Kotlin (legacy version)
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.

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:
- Encoding: We use
QRCodeWriter().encode()to convert the data into aBitMatrix, which is the logical representation of the QR (a grid of turned-on and turned-off points). - Dimensions: We get the width and height of the
BitMatrixto create aBitmapof the exact size. - Creating the Bitmap: We instantiate the object using
createBitmap(). You will see examples online using the old APIBitmap.createBitmap, but the modern way uses Kotlin's extension function with named parameters, specifying the color format:Bitmap.Config.RGB_565(less memory) orARGB_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)theBitMatrixhas active data, we paint that pixel withColor.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:
- Usage of
remember: We callgenerarQR()insideremember(textoParaQR). This is critical so that the QR is not regenerated on every UI recomposition—which would be very expensive performance-wise—. By passingtextoParaQRas a key, the QR will only regenerate when that value changes. - Conversion to
ImageBitmap: Compose'sImagecomposable expects anImageBitmap, not anandroid.graphics.Bitmap. For conversion we use the extension.asImageBitmap(). - User interface: We add the
Imagecomponent with its accessibility description, aSpacerfor visual separation, and a descriptiveText.
@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:

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:
@Composablecomponents 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
CameraXandML Kitfor 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 withLaunchedEffectupon mounting.CameraPreview: wraps anAndroidViewto integrate CameraX'sPreviewView, as there is no native Composable for camera preview yet.BarcodeAnalyzer: implementsImageAnalysis.Analyzerand uses ML Kit to process each frame. It is essential to callimageProxy.close()inaddOnCompleteListenerto release the buffer and not block the camera pipeline.- The result is returned to the main Composable via the
onQrCodeDetectedcallback, 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:
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 thesurfaceCreatedcallback, 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:

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:

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.