Índice de contenido
- ¿Qué es un código QR?
- ¿Para qué sirve un código QR y cómo podemos explotarlo en nuestras apps Android?
- ¿Cómo generar un código QR?
- Generación de códigos QR en Android con Jetpack Compose
- 1. Configuración de la dependencia
- Lógica de la función: de datos a Bitmap
- El concepto de BitMatrix y Bitmap
- Renderizado de la imagen en Kotlin
- Consumo en la interfaz con Jetpack Compose
- Crear un lector de códigos QR con Jetpack Compose
- Ventajas de Jetpack Compose para un lector QR:
- 1. Dependencias (build.gradle del módulo):
- 2. Permisos (AndroidManifest.xml):
- 3. Estructura del Composable (ejemplo completo):
- Enfoque Legacy con XML (para referencia histórica)
- Desarrollando un lector QR con la librería de Play Services
- Escáner QR: ¿cómo descifrar un código QR?
- Desarrollando nuestro propio lector de códigos QR en Android
- Agregando la dependencia en el archivo build.gradle
- Configurando el resto de la aplicación para el lector QR
- El archivo manifest para manejar los permisos
- La vista de nuestro lector de códigos QR
- Configurando el detector y la cámara en nuestra Activity
- El objeto CameraSource para obtener frames de la cámara
- Los permisos en Android 6 y superior
- Lector de códigos QR en Kotlin (versión legacy)
Siguiendo con la generación de imágenes —como vimos antes al trabajar con el Canvas en Android—, en esta entrada veremos cómo generar un código QR directamente desde una aplicación Android usando Jetpack Compose y la librería ZXing.
¿Qué es un código QR?
Antes de meternos de lleno en la implementación, es importante entender qué es un código QR y cómo funciona su estructura. Esto nos ayudará a tomar mejores decisiones de diseño al integrarlo en nuestras apps.
Un código QR (Quick Response) no es más que un sistema para almacenar información en forma de código de barras de dos dimensiones. A diferencia de los códigos de barras tradicionales —que solo pueden leerse de forma horizontal—, un QR codifica datos tanto en sentido horizontal como vertical, lo que le permite almacenar significativamente más información en el mismo espacio.

Los códigos QR son extremadamente versátiles: sirven para administrar inventarios, compartir URLs, datos de contacto, credenciales de acceso y mucho más. Hoy en día son un elemento casi imprescindible en cualquier aplicación de mediana o gran envergadura, tanto por su facilidad de uso como por la amplia adopción que tienen entre los usuarios.
¿Para qué sirve un código QR y cómo podemos explotarlo en nuestras apps Android?
Los códigos QR se utilizan en prácticamente todos los ámbitos: desde el etiquetado de productos físicos como alimentos, medicamentos y dispositivos electrónicos, hasta el marketing digital, los sistemas de pago y la vinculación de cuentas entre plataformas. En una app Android, puedes integrar un lector QR para permitirle al usuario escanear cualquier tipo de código sin necesidad de salir de tu aplicación.
Un caso de uso concreto: imagina una app de farmacia en la que el cliente escanea el código QR de un medicamento y el sistema —con el perfil del usuario ya cargado— indica automáticamente si ese fármaco es compatible con sus tratamientos actuales, sin que el usuario tenga que ingresar ningún dato de forma manual.
¿Cómo generar un código QR?
En internet encontrarás múltiples formas de generar códigos QR: existen librerías para PHP, Java, Python, Kotlin y muchos otros lenguajes. En este blog encontrarás artículos sobre cómo generar QRs con distintas tecnologías; hoy nos centraremos en la generación programática desde Android con ZXing.
Generación de códigos QR en Android con Jetpack Compose
Vamos a aprender a generar códigos QR directamente desde una aplicación Android con Jetpack Compose. Esto es especialmente útil hoy en día, ya que casi cualquier app compleja requiere este tipo de mecanismo: desde plataformas como Netflix (para vincular dispositivos) hasta pasarelas de pago o sistemas de autenticación. Los QRs son la evolución natural de los códigos de barras, y su integración en Android es más sencilla de lo que parece.
1. Configuración de la dependencia
Para generar QRs utilizaremos la librería ZXing (Zebra Crossing), una de las más conocidas del ecosistema Android. Permite tanto escanear como generar códigos QR y de barras.
Abre tu archivo app/build.gradle.kts y añade la siguiente dependencia:
implementation("com.google.zxing:core:3.5.4")Como siempre, verifica que estás usando la versión más reciente: si posicionas el cursor sobre la dependencia en Android Studio, el IDE te sugerirá si hay una versión más nueva disponible. Una vez añadida, pulsa Sync Now y espera a que finalice la sincronización para evitar errores de importación.
Lógica de la función: de datos a Bitmap
Crearemos una función de nivel superior (fuera de cualquier clase o @Composable) para que sea accesible desde cualquier parte del proyecto. Esta función recibirá dos parámetros: el contenido que queremos embeber en el QR (texto, URL o número) y el tamaño en píxeles de la imagen resultante.
El concepto de BitMatrix y Bitmap
La función devolverá un objeto Bitmap. Un Bitmap es una representación matricial de una imagen, donde cada posición (x, y) almacena información de color en formato de píxel. El proceso de generación del QR sigue tres pasos:
- Codificación: Usamos
QRCodeWriter().encode()para convertir los datos en unaBitMatrix, que es la representación lógica del QR (una cuadrícula de puntos encendidos y apagados). - Dimensiones: Obtenemos el ancho y alto de la
BitMatrixpara crear unBitmapdel tamaño exacto. - Creación del Bitmap: Instanciamos el objeto con
createBitmap(). Verás en internet ejemplos con la API antiguaBitmap.createBitmap, pero la forma moderna usa la función de extensión de Kotlin con parámetros nominales, especificando el formato de color:Bitmap.Config.RGB_565(menos memoria) oARGB_8888(con canal alfa, ideal si necesitas transparencia).
Renderizado de la imagen en Kotlin
Para construir la imagen visual, recorremos la BitMatrix píxel por píxel. Como un QR es tradicionalmente blanco y negro, la lógica es simple:
- Si en la posición
(x, y)laBitMatrixtiene un dato activo, pintamos ese píxel deColor.BLACK. - Si no, lo pintamos de
Color.WHITE.
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
}
}La función está envuelta en un bloque try/catch: si la codificación falla (datos vacíos, tamaño inválido, etc.), retornamos null en lugar de romper la app, lo que nos permite manejar el error de forma controlada en la UI.
Consumo en la interfaz con Jetpack Compose
Para mostrar el QR generado en nuestra interfaz, crearemos un @Composable que siga estos pasos:
- Uso de
remember: Llamamos agenerarQR()dentro deremember(textoParaQR). Esto es crítico para que el QR no se regenere en cada recomposición de la UI —lo que sería muy costoso en rendimiento—. Al pasartextoParaQRcomo clave, el QR solo se regenerará cuando ese valor cambie. - Conversión a
ImageBitmap: El composableImagede Compose espera unImageBitmap, no unandroid.graphics.Bitmap. Para la conversión usamos la extensión.asImageBitmap(). - Interfaz de usuario: Añadimos el componente
Imagecon su descripción de accesibilidad, unSpacerpara separación visual y unTextdescriptivo.
@Composable
fun PantallaQR(textoParaQR: String) {
// Generamos el bitmap y lo recordamos para evitar regeneraciones innecesarias
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 la aplicación obtendremos un código QR renderizado directamente en pantalla:

Crear un lector de códigos QR con Jetpack Compose
Para construir un escáner QR con Jetpack Compose, combinaremos CameraX para el acceso a la cámara con ML Kit Barcode Scanning para el análisis de los frames. Esta es la solución moderna y recomendada por Google para cualquier nueva app Android.
Ventajas de Jetpack Compose para un lector QR:
- Declarativo: Defines la UI una sola vez y Compose gestiona los cambios de estado automáticamente, reduciendo el código reactivo manual.
- Menos código: Se requiere significativamente menos código que con el enfoque tradicional de XML + Activities.
- Reutilización: Los
@Composableson fáciles de combinar y reutilizar en distintas partes de la app. - Compatibilidad nativa con Kotlin: Compose fue diseñado desde cero para Kotlin, aprovechando corrutinas, lambdas y funciones de extensión.
- Integración con librerías modernas: Se integra perfectamente con
CameraXyML Kitpara una experiencia de desarrollo actualizada y con soporte activo de Google.
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, es obligatorio declarar el permiso de cámara. El manejo en tiempo de ejecución puede hacerse con rememberPermissionState de Accompanist Permissions o directamente con rememberLauncherForActivityResult de AndroidX, como verás en el ejemplo siguiente.
<uses-permission android:name="android.permission.CAMERA" /> 3. Estructura del Composable (ejemplo completo):
A continuación te mostramos cómo estructurar un @Composable para mostrar la vista previa de la cámara e integrar el análisis de códigos QR. La lógica compleja se abstrae en la clase auxiliar BarcodeAnalyzer, manteniendo el Composable principal limpio y centrado en la 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
// 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...") }
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
hasCameraPermission = isGranted
if (!isGranted) {
qrCodeResult = "Permiso de cámara denegado."
}
}
)
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 = "Resultado: $qrCodeResult", modifier = Modifier.padding(16.dp))
} else {
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()
}
}
}Analicemos las responsabilidades de cada componente:
QRCodeScannerScreen: el@Composableprincipal. Gestiona el estado del permiso y del resultado. Solicita el permiso conLaunchedEffectal montarse.CameraPreview: envuelve unAndroidViewpara integrar elPreviewViewde CameraX, ya que aún no existe un Composable nativo para la vista previa de cámara.BarcodeAnalyzer: implementaImageAnalysis.Analyzery usa ML Kit para procesar cada frame. Es imprescindible llamar aimageProxy.close()enaddOnCompleteListenerpara liberar el buffer y no bloquear el pipeline de la cámara.- El resultado se devuelve al Composable principal mediante el callback
onQrCodeDetected, manteniendo el flujo de datos unidireccional.
Este enfoque moderno simplifica considerablemente la configuración de la UI y mejora la legibilidad del código, aprovechando el paradigma declarativo de Jetpack Compose junto con ML Kit y CameraX.
Enfoque Legacy con XML (para referencia histórica)
En esta sección veremos cómo implementar un lector QR usando el enfoque clásico basado en XML y Activities. Aunque ya no es el método recomendado para nuevos proyectos, es útil para entender la evolución del desarrollo Android o para dar mantenimiento a apps con código legacy.
Desarrollando un lector QR con la librería de Play Services
Google pone a disposición de los desarrolladores una amplia cantidad de APIs a través de sus librerías de Play Services. Para tareas comunes como leer un código QR, no es necesario recurrir a librerías de terceros: la propia librería de Google hace todo el trabajo pesado 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) en combinación con CameraX para la integración de la cámara.
Crear un lector QR personalizado en Android Studio es bastante sencillo: solo debemos incluir la librería externa correspondiente a los servicios de Google Play y configurar algunos componentes.
Escáner QR: ¿cómo descifrar un código QR?
Con la ayuda del móvil podemos recuperar la información almacenada en un QR con solo apuntar la cámara hacia él. Esa es exactamente la idea de lo que implementaremos en esta sección.
Desarrollando nuestro propio lector de códigos QR en Android
Primero debemos agregar la dependencia necesaria en nuestro archivo build.gradle.
Agregando la dependencia en el archivo build.gradle
Abrimos el archivo build.gradle a nivel de módulo 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 e incluye APIs que probablemente no necesites. Es mejor usar solo los módulos requeridos. Además, play-services-vision está deprecada. La opción moderna recomendada es ML Kit Barcode Scanning con CameraX.
Si solo nos interesa la API de vision para leer QRs, podemos usar la dependencia más específica en nuestro build.gradle:
implementation 'com.google.android.gms:play-services-vision:15.0.2'
Las versiones se actualizan con frecuencia. Siempre puedes consultar la última disponible en la documentación oficial:
Nota de actualización: Usar rangos de versión como 15.+ no es una práctica recomendada en producción, ya que puede introducir incompatibilidades inesperadas. Siempre especifica una versión exacta.
Si quisieras que la dependencia se actualice automáticamente a sub-versiones de la 15.x: implementation 'com.google.android.gms:play-services-vision:15.+'
Configurando el resto de la aplicación para el lector QR
El archivo manifest para manejar los permisos
En nuestro AndroidManifest.xml declaramos el uso de la cámara:
<?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>La vista de nuestro lector de códigos QR
Con la dependencia ya configurada, podemos ir a nuestra Activity o Fragment a crear los objetos y configuraciones necesarios. Pero primero, definamos el layout con la vista de la cámara:
<?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>Configurando el detector y la cámara en nuestra Activity
En nuestra Activity o Fragment creamos un objeto de tipo BarcodeDetector. Esta clase permite reconocer códigos de barras y QR a través de la cámara, devolviendo un SparseArray con el dato decodificado. A continuación también creamos el CameraSource:
// Creo el detector QR
barcodeDetector =
new BarcodeDetector.Builder(getContext())
.setBarcodeFormats(Barcode.QR_CODE)
.build();
// Creo la fuente de cámara
cameraSource = new CameraSource
.Builder(getContext(), barcodeDetector)
.setRequestedPreviewSize(640, 480)
.build();El objeto CameraSource para obtener frames de la cámara
Referenciamos el SurfaceView de nuestro layout; este componente refleja en pantalla lo que está captando la cámara del dispositivo:
cameraView = (SurfaceView) v.findViewById(R.id.camera_view);A continuación, registramos el listener del ciclo de vida del SurfaceHolder para iniciar y detener la cámara en el momento correcto:
// Listener de ciclo de vida de la cámara
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();
}
});Un punto clave: la cámara debe iniciarse con
cameraSource.start(cameraView.getHolder())dentro del callbacksurfaceCreated, es decir, una vez que la superficie ya ha sido dibujada. Intentar arrancarla antes provocará un error.
Por último, preparamos el evento que retornará el resultado del QR detectado. Cuando barcodes.size() sea mayor a cero, significa que se ha leído al menos un código:
// 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 con el valor obtenido
}
barcodeDetector.release();
}
});Con barcodes.valueAt(0).displayValue.toString() obtenemos el dato devuelto por el QR: puede ser texto plano, un número o una URL, dependiendo de cómo haya sido generado el código.
Los permisos en Android 6 y superior
Nota de actualización: Las clases ActivityCompat y AppCompatActivity del código original pertenecen a las bibliotecas de soporte legadas (android.support.*). En proyectos modernos han sido reemplazadas por AndroidX (androidx.core.app.ActivityCompat y androidx.appcompat.app.AppCompatActivity). El manejo de permisos sigue un patrón similar, pero siempre conviene consultar la documentación más reciente de AndroidX.
A partir de Android 6 (API 23), Google introdujo un nuevo esquema de permisos en tiempo de ejecución: los permisos sensibles —como el acceso a la cámara— deben solicitarse explícitamente al usuario, no solo declararse en el AndroidManifest.
Por lo tanto, además de la declaración en el manifest, debemos solicitar el permiso desde el código de nuestra Activity. Añadimos esta lógica al construir el SurfaceHolder:
// Listener de ciclo de vida de la cámara
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();
}
});Verificamos si el permiso ya fue concedido con checkSelfPermission y, en caso negativo, mostramos el diálogo nativo con shouldShowRequestPermissionRationale seguido de requestPermissions.
El código completo de la Activity quedaría así:
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() {
BarcodeDetector barcodeDetector =
new BarcodeDetector.Builder(this)
.setBarcodeFormats(Barcode.ALL_FORMATS)
.build();
cameraSource = new CameraSource
.Builder(this, barcodeDetector)
.setRequestedPreviewSize(1600, 1024)
.setAutoFocusEnabled(true) // Recomendado para un escaneo más preciso
.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();
}
}
}
});
}
}Lector de códigos QR en Kotlin (versión legacy)
La misma implementación pero en Kotlin. Nota los cambios idiomáticos: el uso de val y var, la sintaxis object : para implementaciones anónimas, la llamada segura con !! en nulables y la ausencia de punto y coma:
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()
}
}
}
})
}
En el código anterior puedes ver el flujo completo de detección: obtenemos el valor del QR con barcodes.valueAt(0).displayValue.toString(), lo validamos para evitar procesar el mismo código consecutivamente (gracias al mecanismo de tokenanterior y el Thread con wait(5000)), y finalmente con URLUtil.isValidUrl(token) determinamos si el contenido es una URL —en cuyo caso abrimos el navegador— o texto plano, que compartimos con otras apps mediante un Intent.ACTION_SEND.
Al ejecutar la aplicación obtendremos una pantalla como la siguiente:

Recuerda que una vez que obtienes el código puedes hacer lo que necesites con él: enviarlo a tu backend para validarlo, mostrarlo en un dialog o abrir el navegador. Si te interesa generar QRs desde el servidor, puedes revisar el artículo sobre cómo crear códigos QR en CodeIgniter.
Puedes usar el siguiente QR para probar tu escáner; apunta la cámara y debería devolverte la URL de este blog:

Un detalle importante: este mismo código sirve para leer códigos de barras (barcodes) además de QRs. El core del lector es idéntico; simplemente ajustando el parámetro en setBarcodeFormats() puedes soportar EAN-13, UPC-A, Code 128 y muchos más formatos. Con eso, puedes construir una app bastante potente que funcione como lector universal de códigos.
Siguiente paso: aprende a usar Room Database (SQLite) en Android Studio con Kotlin.