Cómo generar códigos QR en Android Studio | Jetpack Compose

Video thumbnail

Siguiendo con la generación de imágenes, como vimos antes con el Canvas en Android, veamos como podemos generar un código QR en Android Studio con composable.

¿Qué es un código QR?

Antes de entrar al tema, de cómo desarrollar un lector QR empleando una librería nativa provista por la gente de Google, es importante saber que es un lector de código QR y cómo funciona su estructura que es lo que explicaremos a continuación.

Un código QR no es más que un sistema que permite almacenar información en una especie de código de barras de última generación

Un código QR es un tipo de código de barra pero en dos dimensiones en el cual se codifica información en un cuadro como el siguiente

Código qr
Ejemplo de un código QR.

Los códigos QR son realmente útiles para administrar inventarios, compartir datos e información de una manera fácil y muy profesional; los códigos QR son actualmente un elemento fundamental en cualquier aplicación de pequeña o gran envergadura debido a su versatilidad al momento de usarlos.

¿Para qué sirve un Código QR? y como podemos explotarlos en nuestras apps Android

Como se comentó anteriormente, los códigos QR son ampliamente empleados debido a su versatilidad y gran utilidad; se emplean en todo tipo de productos como alimentos hasta llegar a dispositivos, automóviles, revistas y otros tantos de tipos de ámbitos publicidad, marketing, etc... y es debido a que con una app Android podemos escanear cualquier tipo de código QR.

Aunque nuestro caso de interés es mostrar mostrar el proceso de creación de una aplicación Android para emplearlo de scanner en una app Android...

...En donde podemos procesar medicinas e indicar al cliente si la medicina es de utilidad para su persona teniendo su perfil previamente cargado y sin necesidad de que el cliente suministre información o datos algunos de manera manual.

¿Cómo generar un código QR?

En Internet encontrarás una cantidad inmensa de formas para generar códigos QR; inclusive podemos crear códigos QR con librerías PHP, Java, Python etc; y existen webs que se encargan de generar códigos QR; en el blog, puedes encontrar varios artículos sobre como generar un QR con distintas tecnologías.

Generación de códigos QR en Android con Jetpack Compose

Vamos a aprender a generar códigos QR desde una aplicación en Android. Esto es particularmente útil hoy en día, ya que casi cualquier aplicación compleja requiere mecanismos de este tipo: desde plataformas multiplataforma tipo Netflix (para vincular dispositivos) hasta pasarelas de pago. Los QRs son la evolución natural de los códigos de barras y se utilizan muchísimo, así que vamos a ver cómo implementarlos.

1. Configuración de la Dependencia

Para esto utilizaremos la librería ZXing (Zebra Crossing), que permite tanto escanear como generar códigos.

app/build.gradle.kts

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

Como siempre, ya tengo parte del código preparado. Primero, agregamos el paquete en el archivo de dependencias. Verifica siempre la versión; si te posicionas encima, el IDE te indicará cuál es la última. Le damos a Sync Now y esperamos a que termine el proceso para evitar errores de importación.

Lógica de la Función: De Datos a Bitmap

Vamos a crear una función para generar el QR fuera de la clase principal, de modo que podamos acceder a ella fácilmente desde nuestros Composables. Esta función recibirá los datos (el texto, URL o número que queremos embeber) y el tamaño de la imagen.

El concepto de BitMatrix y Bitmap

La función generará un Bitmap. Si no estás familiarizado con el término, un Bitmap es básicamente una matriz de bits que representa una imagen. Es una matriz de dos dimensiones (X y Y) donde cada posición contiene información de color.

  1. Codificación: Usamos QRCodeWriter().encode() para convertir los datos en una BitMatrix.
  2. Dimensiones: Obtenemos el ancho y alto de la matriz (usualmente es un cuadrado).
  3. Creación del Bitmap: Configuramos el objeto con Bitmap.createBitmap. Aunque verás en internet formas antiguas, la recomendada actualmente utiliza parámetros con nombre para definir el formato de color (como ARGB_8888).

Renderizado de la Imagen en Kotlin

Para generar la imagen visual, recorremos la matriz píxel por píxel. Como un QR es tradicionalmente blanco y negro, preguntamos a la matriz:

  • Si en la posición (X, Y) existe un dato, pintamos el píxel de negro.
  • Si no, lo pintamos de blanco.
fun generarQR(datos: String, size: Int = 512): Bitmap? {
    return try {
        val writer = QRCodeWriter()
        // Crea la matriz de puntos (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)

        // Recorremos la matriz para pintar píxel por píxel
        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
    }
}

Consumo en la Interfaz (Jetpack Compose)

Para mostrar el QR en nuestra aplicación, utilizaremos una columna y seguiremos estos pasos:

  1. Uso de remember: Llamamos a nuestra función generateQR envolviéndola en un remember. Esto es crucial para que el QR no se genere de nuevo en cada recomposición de la interfaz, lo cual sería muy ineficiente.
  2. Conversión a ImageBitmap: El componente Image de Compose espera un ImageBitmap. Para ello, usamos la función de extensión .asImageBitmap() sobre nuestro Bitmap original.
  3. Interfaz de Usuario: Añadimos un Spacer, un texto descriptivo ("Escanea este código") y finalmente mostramos la imagen.
@Composable
fun PantallaQR(textoParaQR: String) {
    // Generamos el bitmap y lo recordamos
    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 = "Código QR de $textoParaQR",
                modifier = Modifier.size(250.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(text = "Escanea para ver el contenido", style = MaterialTheme.typography.bodyMedium)
        } else {
            Text("Error al generar el QR")
        }
    }
}

Al ejecutar, tendremos un código QR:

desarrollolibre qr
Ejemplo de QR para probar el escáner.

Crear un lector de códigos QR

Para construir un lector QR con Jetpack Compose, utilizaríamos una combinación de librerías modernas para la cámara y el escaneo de códigos de barras.

Ventajas de Jetpack Compose para un Lector QR:

  • Declarativo: Define la UI de una vez, Compose se encarga de los cambios.
  • Menos Código: A menudo se requiere menos código para lograr el mismo resultado que con XML y Java/Kotlin tradicional.
  • Reutilización: Los Composables son fáciles de reutilizar y combinar.
  • Compatibilidad con Kotlin: Diseñado desde cero para Kotlin, aprovechando sus características.
  • Integración con Librerías Modernas: Se integra perfectamente con CameraX y ML Kit para una experiencia de desarrollo actualizada.

1. Dependencias (build.gradle del módulo):

// 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' // O la versión más reciente

// CameraX
implementation 'androidx.camera:camera-core:1.3.2' // O la versión más reciente
implementation 'androidx.camera:camera-camera2:1.3.2'
implementation 'androidx.camera:camera-lifecycle:1.3.2'
implementation 'androidx.camera:camera-view:1.3.2' // Para CameraPreview Composable

// ML Kit Barcode Scanning
implementation 'com.google.mlkit:barcode-scanning:17.2.0' // O la versión más reciente
// Para modelos con Google Play Services (reconocimiento más rápido/preciso)
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0'

2. Permisos (AndroidManifest.xml):

Al igual que en el enfoque legacy, se necesita el permiso de cámara. El manejo de permisos en tiempo de ejecución se haría usando librerías o directamente con rememberPermissionState de Accompanist Permissions o una implementación similar.

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

3. Estructura de un Composable (Pseudo-código):

Aquí te mostramos cómo se estructuraría un Composable para mostrar la vista previa de la cámara e integrar el escaneo de códigos de barras. La lógica compleja de la cámara y el análisis de imágenes se abstraería en funciones o clases de ayuda.

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

// Composable principal para el lector QR
@Composable
fun QRCodeScannerScreen() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    var hasCameraPermission by remember { mutableStateOf(false) }
    var qrCodeResult by remember { mutableStateOf("Escaneando...") }

    // Launcher para solicitar permisos de cámara
    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { isGranted ->
            hasCameraPermission = isGranted
            if (!isGranted) {
                qrCodeResult = "Permiso de cámara denegado."
            }
        }
    )

    // Solicitar permiso al iniciar el Composable
    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
                // Aquí podrías agregar lógica para navegar, mostrar un diálogo, etc.
            })
            Spacer(Modifier.height(16.dp))
            Text(text = "Resultado: $qrCodeResult", modifier = Modifier.padding(16.dp))
        } else {
            // UI cuando no hay permiso
            Text("Necesitamos permiso de cámara para escanear QR.", modifier = Modifier.padding(16.dp))
            Button(onClick = { permissionLauncher.launch(Manifest.permission.CAMERA) }) {
                Text("Conceder Permiso")
            }
        }
    }
}

@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) {
                    // Manejar errores de inicialización de cámara
                }
            }, ContextCompat.getMainExecutor(ctx))
            previewView
        }
    )
    DisposableEffect(Unit) {
        onDispose {
            cameraExecutor.shutdown()
        }
    }
}

// Clase para analizar los frames de la cámara
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() // Importante cerrar ImageProxy para liberar el buffer
                }
        } else {
            imageProxy.close()
        }
    }
}

En este ejemplo conceptual:

  • QRCodeScannerScreen es el Composable principal que maneja los permisos y el estado del resultado.
  • CameraPreview es un Composable que envuelve un AndroidView para integrar la PreviewView de CameraX, ya que no hay un Composable directo para la vista previa de la cámara aún.
  • BarcodeAnalyzer es una clase que implementa ImageAnalysis.Analyzer de CameraX y utiliza el ML Kit Barcode Scanning para procesar los frames de la cámara y detectar códigos QR.
  • El resultado del escaneo se pasa de vuelta al Composable principal a través de un callback.

Este enfoque moderno simplifica significativamente la configuración de la UI y mejora la legibilidad y mantenibilidad del código, aprovechando el paradigma declarativo de Jetpack Compose y las capacidades avanzadas de ML Kit y CameraX.

Enfoque Legacy con XML

En esta entrada veremos cómo crear un lector de códigos QR en Android en nuestras aplicaciones; para hacernos una idea más clara del funcionamiento de un lector QR, actualmente existen varios lectores QR en Android que los puedes encontrar en la Google Play y en este apartado, vamos a implementar uno, aunque, para lectores, puedes usar la app de la camara.

Desarrollando un lector QR en nuestra aplicación Android con la librería de Play Service

Como bien sabrás, Google ofrece una importante cantidad de funcionalidades que se traducen en várias API desarrolladas por el propio Google que pone a nuestra disposición de nosotros los desarrolladores mediante sus librerías así como la de Play Services así también como la de soportes entre muchas más; para hacer las labores más comunes como crear una aplicación para leer un código QR no es necesario emplear una librería o paquete de un tercero; nos basta con la que provee Google que hace todo el trabajo por nosotros.

Nota de Actualización: La librería `com.google.android.gms:play-services-vision` ha sido deprecada. Para nuevas implementaciones, Google recomienda utilizar ML Kit Barcode Scanning (`com.google.mlkit:barcode-scanning` o `com.google.android.gms:play-services-mlkit-barcode-scanning` para capacidades adicionales de Google Play Services) en combinación con CameraX para la integración de la cámara.

Crear un lector QR personalizado (creado por nosotros mismos empleando como ambiente de desarrollo de software o IDE a Android Studio) es bastante sencillo con Android y solo debemos incluir una librería externa la cual corresponde a los servicios de Google Play: com.google.android.gms:play-services-vision, pero antes de esto veamos qué es, cómo funciona un código QR y cómo podemos emplearlo en nuestras aplicaciones Android y webs:

Scanner del QR: ¿Cómo descifrar un código QR?

Con la ayuda de un móvil podemos recuperar esta información tan solo con apuntar la cámara hacia el código QR y esa es la idea y lo que haremos en la siguiente sección de esta entrada en donde nos dedicaremos a detallar a como crear un lector QR con Android.

Desarrollando nuestro propio lector de códigos QR en Android

Finalmente llegamos al área de interés en cuál es crear nuestro lector de códigos QR en Android; primero debemos agregar la dependencia necesaria en nuestro build.gradle.

Agregando la dependencia en el archivo build.gradle en Android Studio

Abrimos nuestro archivo build.gradle y agregamos la siguiente dependencia:

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

Nota de Actualización: La dependencia play-services es demasiado amplia. Es mejor usar solo los módulos necesarios. Además, la versión 15.0.2 es muy antigua. La librería de Google Play Services Vision está deprecada. Si se insiste en usar una versión similar de Vision API, se buscaría una más reciente y específica como com.google.android.gms:play-services-vision:X.Y.Z (donde X.Y.Z es una versión reciente antes de su deprecación total) o, mejor aún, migrar a ML Kit Barcode Scanning y CameraX.

La anterior dependencia nos da acceso no solo a la API de vision si no a toda la plataforma que nos ofrece la Google Play como en nuestro caso de interés es solomenteleer un código QR, podemos emplear la siguiente dependencia en nuestro build.gradle:

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

Debes tener presente que las versiones cambian de un día a otro; puedes encontrar la última versión en la documentación oficial en el siguiente enlace:

Set Up Google Play Services

Nota de Actualización: El uso de 15.+ para versiones de dependencia no es una práctica recomendada en proyectos de producción, ya que puede introducir problemas de compatibilidad y compilación inesperados. Es mejor especificar una versión exacta o usar rangos con precaución.

Para que el paquete actualice automáticamente podríamos hacer algo así implementation 'com.google.android.gms:play-services-vision:15.+'

Configurando el resto de la aplicación para nuestro lector QR

El archivo manifest para manejar los permisos

En nuestro manifest colocamos el uso de la cámara:

"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>

La vista de nuestro lector de códigos QR

Con nuestra dependencia ya configurada ahora podemos ir a nuestra actividad y/o fragment y crear los objetos, eventos y configuraciones necesarias para tener nuestro propio lector QR; pero primero debemos de configurar el layout del a actividad o fragment:

"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>

Configurando el lector de código y la cámara en nuestra actividad

Y en nuestra actividad o fragment debemos crear un objeto de tipo BarcodeDetector esta clase permite reconocer códigos de barra y códigos QR -el cual es nuestro caso de interés- a través de la cámara del dispositivo y devolver un objeto de tipo SparseArray con el dato decodificado del QR analizado además de todo esto, creamos la fuente de la cámara y la resolución asociada:

        // creo el detector qr
        barcodeDetector =
                new BarcodeDetector.Builder(getContext())
                        .setBarcodeFormats(Barcode.QR_CODE)
                        .build();
        // creo la camara fuente
        cameraSource = new CameraSource
                .Builder(getContext(), barcodeDetector)
                .setRequestedPreviewSize(640, 480)
                .build();

El objeto CameraSource para obtener frames de la cámara

Especificamos el objeto SurfaceView de nuestro layout; este objeto se encarga de espejar o reflejar lo que estamos viendo por la cámara del dispositivo en nuestra superficie de nuestro layout:

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

Ahora especificamos los listener o escuchadores que permiten controlar el ciclo de vida de la cámara:

        // listener de ciclo de vida de la camara
        cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                // verifico si el usuario dio los permisos para la camara
                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();
            }
        });

Un punto importante del código anterior es iniciar la cámara una vez que nuestra superficie sea dibujada con cameraSource.start(cameraView.getHolder());.

Por último se prepara el evento que retornará el resultado provisto por la detección y procesamiento del QR; cuando existe una respuesta barcodes.size() nos devolverá un valor mayor a cero:

 // preparo el detector de QR
        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();
                 // hacer algo
            }
barcodeDetector.release();
        });

Con el barcodes.valueAt(0).displayValue.toString(); obtenemos el dato devuelto por el QR que puede ser un texto, entero o una URL dependiendo cómo configuremos nuestro código QR.

Los permisos en Android 6 y superior

Nota de Actualización: Las clases ActivityCompat y AppCompatActivity en el código original son de las bibliotecas de soporte (android.support.*). En proyectos modernos, estas han sido reemplazadas por AndroidX (androidx.core.app.ActivityCompat y androidx.appcompat.app.AppCompatActivity, respectivamente). El manejo de permisos sigue un patrón similar, pero siempre es bueno consultar la documentación más reciente de AndroidX.

A partir de Android 6 Google creó un nuevo esquemas de permisos los cuales son solicitados al usuarios al momento de requerir emplear alguna función de dicho(s) permiso(s)...

...por lo tanto no basta con solo incluir la dependencia en nuestro AndroidManifest, también debemos solicitar el permiso propiamente dicho desde el código java de nuestra aplicación Android.

Entonces, al momento de construir nuestra superficie colocamos el siguiente código:

// listener de ciclo de vida de la camara
cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
   @Override
   public void surfaceCreated(SurfaceHolder holder) {
       // verifico si el usuario dio los permisos para la camara
       if (ActivityCompat.checkSelfPermission(QRActiviy.this, Manifest.permission.CAMERA)
               != PackageManager.PERMISSION_GRANTED) {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
               // verificamos la version de Android que sea al menos la M para mostrar
               // el dialog de la solicitud de la camara
               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();
   }
});

El código es relativamente sencillo, verificamos si el permiso fue concedido con checkSelfPermission y con shouldShowRequestPermissionRationale mostramos el dialog para solicitar el permiso.

Finalmente el código queda de la siguiente manera:

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; // Usar androidx.core.app.ActivityCompat
import android.support.v7.app.AppCompatActivity; // Usar 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; // Deprecado, usar ML Kit Barcode Scanning
import com.google.android.gms.vision.Detector; // Deprecado, usar ML Kit Barcode Scanning
import com.google.android.gms.vision.barcode.Barcode; // Deprecado, usar ML Kit Barcode Scanning
import com.google.android.gms.vision.barcode.BarcodeDetector; // Deprecado, usar 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() {
        // creo el detector qr
        BarcodeDetector barcodeDetector =
                new BarcodeDetector.Builder(this)
                        .setBarcodeFormats(Barcode.ALL_FORMATS)
                        .build();
        // creo la camara
        cameraSource = new CameraSource
                .Builder(this, barcodeDetector)
                .setRequestedPreviewSize(1600, 1024)
                .setAutoFocusEnabled(true) //you should add this feature
                .build();
        // listener de ciclo de vida de la camara
        cameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                // verifico si el usuario dio los permisos para la camara
                if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
                        != PackageManager.PERMISSION_GRANTED) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        // verificamos la version de ANdroid que sea al menos la M para mostrar
                        // el dialog de la solicitud de la camara
                        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();
            }
        });
        // preparo el detector de QR
        barcodeDetector.setProcessor(new Detector.Processor() {
            @Override
            public void release() {
            }
            @Override
            public void receiveDetections(Detector.Detections detections) {
                final SparseArray barcodes = detections.getDetectedItems();
                if (barcodes.size() > 0) {
                    // obtenemos el token
                    token = barcodes.valueAt(0).displayValue.toString();
                    // verificamos que el token anterior no se igual al actual
                    // esto es util para evitar multiples llamadas empleando el mismo token
                    if (!token.equals(tokenanterior)) {
                        // guardamos el ultimo token proceado
                        tokenanterior = token;
                        Log.i("token", token);
                        if (URLUtil.isValidUrl(token)) {
                            // si es una URL valida abre el navegador
                            Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(token));
                            startActivity(browserIntent);
                        } else {
                            // comparte en otras apps
                            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);
                                        // limpiamos el token
                                        tokenanterior = "";
                                    }
                                } catch (InterruptedException e) {
                                    Log.e("Error", "Waiting didn't work!!"); // Mensaje corregido
                                    e.printStackTrace();
                                }
                            }
                        }).start();
                    }
                }
            }
        });
    }
}

Lector de códigos QR para Kotlin

Toda la implementación anterior pero en vez de Java sería para Kotlin todo nuestro código quedaría de la siguiente manera:

 private var cameraSource: CameraSource? = null // NULL corregido a null
    private var cameraView: SurfaceView? = null // NULL corregido a 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(R.id.camera_view)
        initQR()
    }
    fun initQR() {
        // creo el detector qr
        val barcodeDetector = BarcodeDetector.Builder(this)
                .setBarcodeFormats(Barcode.ALL_FORMATS)
                .build()
        // creo la camara
        cameraSource = CameraSource.Builder(this, barcodeDetector)
                .setRequestedPreviewSize(1600, 1024)
                .setAutoFocusEnabled(true) //you should add this feature
                .build()
        // listener de ciclo de vida de la camara
        cameraView!!.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {
                // verifico si el usuario dio los permisos para la camara
                if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        // verificamos la version de ANdroid que sea al menos la M para mostrar
                        // el dialog de la solicitud de la camara
                        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()
            }
        })
        // preparo el detector de QR
        barcodeDetector.setProcessor(object : Detector.Processor { // Añadido override
            override fun release() {} // Añadido override
            override fun receiveDetections(detections: Detector.Detections<Barcode>) { // Añadido override
                val barcodes = detections.getDetectedItems()
                if (barcodes.size() > 0) { // Movido el acceso a barcodes.valueAt(0) dentro del if
                    // obtenemos el token
                    token = barcodes.valueAt(0).displayValue.toString()
                    // verificamos que el token anterior no se igual al actual
                    // esto es util para evitar multiples llamadas empleando el mismo token
                    if (token != tokenanterior) {
                        // guardamos el ultimo token proceado
                        tokenanterior = token
                        Log.i("token", token)
                        if (URLUtil.isValidUrl(token)) {
                            // si es una URL valida abre el navegador
                            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(token))
                            startActivity(browserIntent)
                        } else {
                            // comparte en otras apps
                            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)
                                        // limpiamos el token
                                        tokenanterior = ""
                                    }
                                } catch (e: InterruptedException) {
                                    Log.e("Error", "Waiting didn't work!!") // Mensaje corregido
                                    e.printStackTrace()
                                }
                            }
                        }).start()
                    }
                }
            }
        })
    }

En el código anterior puede notar que al momento de detectar una lectura del código QR por el móvil se realiza una serie de pasos que va desde la obtención del mismo mediante barcodes.valueAt(0).displayValue.toString();, la validación (y liberación con el Thread) para evitar leer el mismo QR de forma consecutiva (el lector QR siempre sigue ejecutándose independientemente de si ha procesado un QR o no) y luego mediante URLUtil.isValidUrl(token) verificamos si es una URL el token obtenido, y en ese caso habre en navegador predefinido de nuestro teléfono, en caso contrario simplemente mostramos el dialog para compartir contenido por las distintas aplicaciones que tengamos en nuestro teléfono.

Al correr la aplicación, obtendremos una pantalla como la siguiente:

app corriendo con lector qr
Captura de pantalla de la aplicación ejecutándose con el lector QR (enfoque legacy).

Cabe recordar que una vez que obtenemos el código podemos hacer lo que queramos, generalmente si estamos creando una aplicación personalizada (Ve como crear códigos QR en CodeIgniter), es probable que queramos enviarlo a nuestra aplicación para validarlo o simplemente mostrarlo al usuario mediante un dialog o abrir un navegador como especificamos anteriormente.

Puedes emplear el siguiente QR que devuelve la URL de este Blog:

desarrollolibre qr
Ejemplo de QR para probar el escáner.

Un punto importante es que puedes emplear la misma funcionalidad para leer los barcode o código de barra en Android, se emplea el mismo core de la app y de esta manera puedes crear una app bastante potente que sirva como reader o lector de códigos QR mediante la implementación de la librería y la cámara del dispositivo.

Siguiente paso, aprende a cómo usar Room Database (SQLite) en Android Studio con Kotlin

Aprende a generar códigos QR en Android con ZXing y Kotlin. Guía paso a paso para crear Bitmaps, configurar BitMatrix y mostrar QRs en Jetpack Compose de forma eficiente

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english