Resposta rápida: para criar um leitor de QR Code ou código de barras dentro do seu app Android, combine CameraX para exibir e analisar a câmera com o Barcode Scanning do ML Kit para reconhecer o conteúdo de cada frame. Em Kotlin, configure um ImageAnalysis com STRATEGY_KEEP_ONLY_LATEST, transforme o ImageProxy em InputImage, envie a imagem ao scanner e sempre chame imageProxy.close() ao terminar. Em Jetpack Compose, hospede o PreviewView em um AndroidView, mantenha permissão e resultado no estado da tela e impeça leituras duplicadas enquanto o mesmo código continua diante da câmera. Restrinja os formatos aceitos quando possível: isso reduz trabalho, acelera a detecção e torna o comportamento mais previsível.

Esse desenho atende checkout, inventário, entrada em eventos, identificação de produtos, configuração por QR Code, logística e conferência de documentos. O ponto mais difícil não é reconhecer um código isolado; é operar uma câmera contínua sem acumular frames, bloquear a interface, disparar a mesma ação várias vezes ou confiar cegamente no texto lido.

Neste guia, você vai montar o fluxo completo com Kotlin, CameraX, ML Kit e Compose, separar análise da UI, controlar duplicidade, validar conteúdo, testar sem depender apenas da câmera real e decidir quando um scanner próprio vale mais que uma solução pronta do sistema.

Como CameraX e ML Kit trabalham juntos

As duas bibliotecas têm responsabilidades diferentes:

ComponenteResponsabilidade
CameraX Previewmostrar o enquadramento ao usuário
CameraX ImageAnalysisentregar frames para processamento
ML Kit Barcode Scanninglocalizar e decodificar QR Codes e códigos de barras
ViewModelcontrolar estado, repetição e efeitos de navegação
Jetpack Composerenderizar permissão, mira, resultado e erros

CameraX não interpreta o código. ML Kit não controla o lifecycle da câmera. A integração conecta o frame produzido por ImageAnalysis ao scanner e devolve um resultado de domínio para a aplicação.

Se você ainda não montou preview e permissão, comece pelo guia de CameraX com Jetpack Compose e Kotlin. Ele explica PreviewView, bindToLifecycle, captura e rotação. Aqui, o foco é a análise contínua para códigos.

Scanner próprio ou Google Code Scanner?

Antes de escrever a integração completa, defina o nível de controle necessário.

Um scanner baseado em CameraX e ML Kit faz sentido quando o produto precisa de:

  • preview personalizado dentro da própria tela;
  • moldura, instruções e feedback visual próprios;
  • leitura contínua de vários itens;
  • integração com inventário ou fluxo offline;
  • seleção de formatos específicos;
  • análise adicional antes de aceitar o resultado;
  • telemetria e estados de domínio controlados pelo app.

Quando o requisito é apenas “abrir um scanner, ler um código e receber o valor”, avalie uma solução de scanner fornecida pelos serviços do Google, quando disponível para os dispositivos suportados. Ela reduz código de câmera e pode evitar a necessidade de seu app pedir permissão diretamente. A troca é menos controle sobre a experiência e sobre a disponibilidade.

Também não use câmera própria se a pessoa já possui uma imagem com o QR Code. Nesse caso, selecione a mídia com o Photo Picker no Android e execute reconhecimento sobre a imagem autorizada, sem abrir preview contínuo.

Dependências do projeto

Centralize versões no Version Catalog e escolha as versões estáveis compatíveis com seu Android Gradle Plugin. O exemplo usa placeholders de propósito:

# gradle/libs.versions.toml
[versions]
cameraX = "<versao-estavel-atual>"
mlKitBarcode = "<versao-estavel-atual>"

[libraries]
androidx-camera-camera2 = {
  module = "androidx.camera:camera-camera2",
  version.ref = "cameraX"
}
androidx-camera-lifecycle = {
  module = "androidx.camera:camera-lifecycle",
  version.ref = "cameraX"
}
androidx-camera-view = {
  module = "androidx.camera:camera-view",
  version.ref = "cameraX"
}
google-mlkit-barcode-scanning = {
  module = "com.google.mlkit:barcode-scanning",
  version.ref = "mlKitBarcode"
}
// build.gradle.kts do módulo Android
dependencies {
    implementation(libs.androidx.camera.camera2)
    implementation(libs.androidx.camera.lifecycle)
    implementation(libs.androidx.camera.view)
    implementation(libs.google.mlkit.barcode.scanning)
}

Consulte a documentação oficial do Barcode Scanning no ML Kit antes de fixar versões e opções. Alguns modelos podem ser empacotados com o app ou entregues por serviços do Google, conforme o artefato escolhido. Isso afeta tamanho inicial, disponibilidade offline e comportamento da primeira execução.

No manifesto, declare a câmera:

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

<uses-feature
    android:name="android.hardware.camera.any"
    android:required="false" />

Use required="true" somente se o app inteiro não fizer sentido sem câmera. Se o scanner é uma função adicional, manter false permite instalação em mais dispositivos e tratamento de indisponibilidade dentro do produto.

Peça permissão no contexto certo

Não abra a câmera antes da permissão. Explique a finalidade junto da ação: “Usamos a câmera para ler o código do produto” é melhor que pedir acesso na primeira abertura sem contexto.

Em Compose, o fluxo pode ser modelado com a Activity Result API:

@Composable
fun CameraPermissionGate(
    hasPermission: Boolean,
    shouldShowRationale: Boolean,
    onPermissionResult: (Boolean) -> Unit,
    content: @Composable () -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = onPermissionResult,
    )

    when {
        hasPermission -> content()

        shouldShowRationale -> PermissionExplanation(
            message = "A câmera é usada somente para ler o código.",
            onContinue = {
                launcher.launch(Manifest.permission.CAMERA)
            },
        )

        else -> Button(
            onClick = {
                launcher.launch(Manifest.permission.CAMERA)
            },
        ) {
            Text("Permitir câmera")
        }
    }
}

Depois de uma negativa permanente, mostre como abrir as configurações, mas não transforme a tela em um ciclo de pedidos. O guia de permissões modernas no Android com Kotlin detalha negativa, rationale e estados por versão.

Configure o scanner para os formatos necessários

Por padrão, um scanner pode tentar reconhecer muitos formatos. Se o produto só aceita QR Code e EAN de produtos, declare isso explicitamente:

val options = BarcodeScannerOptions.Builder()
    .setBarcodeFormats(
        Barcode.FORMAT_QR_CODE,
        Barcode.FORMAT_EAN_13,
        Barcode.FORMAT_EAN_8,
    )
    .build()

val scanner = BarcodeScanning.getClient(options)

Formatos comuns incluem:

  • QR Code;
  • EAN-13 e EAN-8;
  • UPC-A e UPC-E;
  • Code 128;
  • Code 39;
  • Data Matrix;
  • PDF417;
  • Aztec.

Selecionar apenas o necessário reduz falsos positivos e pode melhorar o trabalho do detector. A escolha também deve refletir a regra do backend. Um sistema que espera GTIN de produto não deveria aceitar qualquer texto de QR Code e tentar “descobrir depois”.

Crie um analyzer que sempre libera o frame

O analyzer recebe ImageProxy. O objeto representa um frame que precisa ser fechado. Se você esquecer close(), o pipeline para de entregar imagens ou acumula pressão até parecer que a câmera congelou.

class BarcodeAnalyzer(
    private val scanner: BarcodeScanner,
    private val onBarcodesDetected: (List<DetectedBarcode>) -> Unit,
    private val onFailure: (Throwable) -> Unit,
) : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image

        if (mediaImage == null) {
            imageProxy.close()
            return
        }

        val inputImage = InputImage.fromMediaImage(
            mediaImage,
            imageProxy.imageInfo.rotationDegrees,
        )

        scanner.process(inputImage)
            .addOnSuccessListener { barcodes ->
                val detected = barcodes.mapNotNull { barcode ->
                    val raw = barcode.rawValue ?: return@mapNotNull null

                    DetectedBarcode(
                        rawValue = raw,
                        displayValue = barcode.displayValue,
                        format = barcode.format,
                        valueType = barcode.valueType,
                    )
                }

                if (detected.isNotEmpty()) {
                    onBarcodesDetected(detected)
                }
            }
            .addOnFailureListener(onFailure)
            .addOnCompleteListener {
                imageProxy.close()
            }
    }
}

data class DetectedBarcode(
    val rawValue: String,
    val displayValue: String?,
    val format: Int,
    val valueType: Int,
)

Use addOnCompleteListener para garantir o fechamento tanto no sucesso quanto na falha. Não guarde ImageProxy, mediaImage ou referências ao frame depois que a tarefa terminou.

O rotationDegrees também é essencial. A câmera pode fornecer buffers em uma orientação diferente da tela; o ML Kit usa essa informação para interpretar a imagem corretamente.

Controle backpressure e concorrência

Para um scanner em tempo real, normalmente você quer analisar a imagem mais recente, não uma fila de frames antigos:

val analysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(
        ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
    )
    .build()

Depois, associe um executor dedicado:

val cameraExecutor = Executors.newSingleThreadExecutor()

analysis.setAnalyzer(
    cameraExecutor,
    BarcodeAnalyzer(
        scanner = scanner,
        onBarcodesDetected = onBarcodesDetected,
        onFailure = onAnalysisFailure,
    ),
)

Um executor de thread única evita análise concorrente desnecessária. Ainda assim, scanner.process() é assíncrono. Dependendo da taxa de frames e da implementação, você pode proteger o analyzer para não iniciar uma nova tarefa antes da anterior terminar.

class ThrottledBarcodeAnalyzer(
    private val scanner: BarcodeScanner,
    private val onDetected: (List<DetectedBarcode>) -> Unit,
) : ImageAnalysis.Analyzer {

    private val processing = AtomicBoolean(false)

    override fun analyze(imageProxy: ImageProxy) {
        if (!processing.compareAndSet(false, true)) {
            imageProxy.close()
            return
        }

        val image = imageProxy.image
        if (image == null) {
            processing.set(false)
            imageProxy.close()
            return
        }

        val input = InputImage.fromMediaImage(
            image,
            imageProxy.imageInfo.rotationDegrees,
        )

        scanner.process(input)
            .addOnSuccessListener { barcodes ->
                val results = barcodes.mapNotNull { barcode ->
                    barcode.rawValue?.let {
                        DetectedBarcode(
                            rawValue = it,
                            displayValue = barcode.displayValue,
                            format = barcode.format,
                            valueType = barcode.valueType,
                        )
                    }
                }
                if (results.isNotEmpty()) onDetected(results)
            }
            .addOnCompleteListener {
                processing.set(false)
                imageProxy.close()
            }
    }
}

Essa proteção prefere descartar frames a atrasar a experiência. Para código de barras, uma leitura atual é mais útil que processar a imagem de 700 milissegundos atrás.

Vincule Preview e ImageAnalysis ao lifecycle

Uma função de infraestrutura pode montar os casos de uso:

fun bindBarcodeCamera(
    context: Context,
    lifecycleOwner: LifecycleOwner,
    previewView: PreviewView,
    analyzer: ImageAnalysis.Analyzer,
    executor: Executor,
    onBound: () -> Unit,
    onError: (Throwable) -> Unit,
) {
    val providerFuture = ProcessCameraProvider.getInstance(context)

    providerFuture.addListener({
        runCatching {
            val provider = providerFuture.get()

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

            val analysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(
                    ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
                )
                .build()
                .also {
                    it.setAnalyzer(executor, analyzer)
                }

            provider.unbindAll()
            provider.bindToLifecycle(
                lifecycleOwner,
                CameraSelector.DEFAULT_BACK_CAMERA,
                preview,
                analysis,
            )
        }.onSuccess {
            onBound()
        }.onFailure(onError)
    }, ContextCompat.getMainExecutor(context))
}

bindToLifecycle pausa e retoma os casos de uso com a tela. Não mantenha a câmera ativa quando o scanner deixou de estar visível. Ao sair definitivamente, limpe o analyzer, encerre o executor criado para a tela e feche o BarcodeScanner quando ele não for mais reutilizado.

Exiba o PreviewView no Jetpack Compose

CameraX usa PreviewView, portanto Compose precisa hospedá-lo com AndroidView:

@Composable
fun BarcodeCameraPreview(
    analyzer: ImageAnalysis.Analyzer,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val executor = remember {
        Executors.newSingleThreadExecutor()
    }

    DisposableEffect(Unit) {
        onDispose {
            executor.shutdown()
        }
    }

    AndroidView(
        modifier = modifier,
        factory = { viewContext ->
            PreviewView(viewContext).apply {
                scaleType = PreviewView.ScaleType.FILL_CENTER
                implementationMode =
                    PreviewView.ImplementationMode.COMPATIBLE

                bindBarcodeCamera(
                    context = context,
                    lifecycleOwner = lifecycleOwner,
                    previewView = this,
                    analyzer = analyzer,
                    executor = executor,
                    onBound = {},
                    onError = { error ->
                        // Encaminhe para o estado da tela.
                    },
                )
            }
        },
    )
}

Em produção, evite recriar scanner e analyzer a cada recomposição. Crie objetos estáveis com remember, injete uma classe controladora ou mantenha a infraestrutura fora do composable. A UI deve observar estados como Waiting, Scanning, Found e Error, não gerenciar detalhes de ImageProxy.

Modele o estado da tela

Uma sealed interface deixa os estados explícitos:

sealed interface ScannerUiState {
    data object WaitingPermission : ScannerUiState
    data object Scanning : ScannerUiState
    data class Found(
        val value: String,
        val type: BarcodeContentType,
    ) : ScannerUiState
    data class Invalid(val reason: String) : ScannerUiState
    data class Error(val message: String) : ScannerUiState
}

enum class BarcodeContentType {
    Product,
    AppLink,
    PlainText,
}

O analyzer entrega dados brutos; o ViewModel aplica regras:

class ScannerViewModel(
    private val validateBarcode: ValidateBarcode,
) : ViewModel() {

    private val _state = MutableStateFlow<ScannerUiState>(
        ScannerUiState.Scanning
    )
    val state: StateFlow<ScannerUiState> = _state.asStateFlow()

    fun onDetected(items: List<DetectedBarcode>) {
        val first = items.firstOrNull() ?: return

        when (val result = validateBarcode(first.rawValue)) {
            is BarcodeValidation.Valid -> {
                _state.value = ScannerUiState.Found(
                    value = result.normalizedValue,
                    type = result.type,
                )
            }
            is BarcodeValidation.Invalid -> {
                _state.value = ScannerUiState.Invalid(result.reason)
            }
        }
    }

    fun scanAgain() {
        _state.value = ScannerUiState.Scanning
    }
}

Esse desenho permite testar validação sem CameraX e trocar a origem da leitura por scanner externo, digitação manual ou imagem da galeria.

Evite o mesmo resultado disparado dezenas de vezes

Enquanto o QR Code estiver no enquadramento, o detector pode reconhecê-lo em vários frames consecutivos. Sem controle, seu app navega repetidamente, toca vários sons ou envia diversas requisições.

Existem três estratégias principais.

Pare a análise depois do primeiro resultado

É a melhor escolha para fluxos de uso único, como login por QR Code ou entrada em evento. Ao aceitar um resultado, remova o analyzer ou mude um sinal atômico para ignorar novas leituras até a tela sair.

Aplique cooldown

Para inventário, aceite uma leitura e bloqueie o mesmo valor por um intervalo curto:

class DuplicateGuard(
    private val cooldownMillis: Long = 1_500L,
    private val now: () -> Long = SystemClock::elapsedRealtime,
) {
    private var lastValue: String? = null
    private var lastAcceptedAt: Long = 0L

    fun shouldAccept(value: String): Boolean {
        val current = now()
        val duplicate = value == lastValue &&
            current - lastAcceptedAt < cooldownMillis

        if (duplicate) return false

        lastValue = value
        lastAcceptedAt = current
        return true
    }
}

Exija confirmação do usuário

Quando a ação é sensível, mostre o conteúdo e peça confirmação antes de abrir URL, efetuar baixa de estoque ou vincular uma conta. Reconhecimento automático não precisa significar execução automática.

Valide o conteúdo como entrada não confiável

Um QR Code pode conter qualquer texto. Nunca abra uma URL, executar uma ação ou enviar um identificador ao backend apenas porque o ML Kit conseguiu decodificá-lo.

Para URLs:

  • aceite somente https quando possível;
  • valide host e caminho esperados;
  • rejeite credenciais embutidas na URL;
  • limite tamanho do valor;
  • não confie no texto exibido como se fosse o destino real;
  • peça confirmação antes de sair do app;
  • trate deep links com a mesma autenticação usada em qualquer outra entrada.
fun parseTrustedAppLink(raw: String): Uri? {
    val uri = runCatching { raw.toUri() }.getOrNull() ?: return null

    val trusted = uri.scheme == "https" &&
        uri.host == "app.exemplo.com" &&
        uri.userInfo == null &&
        raw.length <= 2_048

    return uri.takeIf { trusted }
}

Para códigos de produto, normalize caracteres e valide tamanho ou dígito verificador antes de consultar. Para tokens de autenticação, prefira valores curtos, descartáveis, vinculados à sessão e validados no backend. Nunca coloque segredo duradouro em QR Code público.

Se a leitura abre uma rota interna, aplique as mesmas regras do guia de App Links e deep links no Android. O scanner é apenas mais uma porta de entrada.

Melhore a experiência de leitura

A qualidade do scanner depende tanto da UI quanto do detector.

Mostre uma área de enquadramento

Uma moldura ajuda o usuário a posicionar o código, mesmo que o analyzer receba o frame inteiro. Se você desenhar overlay sobre o preview, mantenha contraste e não cubra informações essenciais.

Dê feedback imediato

Ao aceitar uma leitura, use feedback visual, som opcional ou vibração respeitando configurações do aparelho. Não vibre em todo frame reconhecido; faça isso apenas quando o DuplicateGuard aceitar o valor.

Ofereça lanterna

Em ambientes escuros, exponha um botão de torch quando a câmera informar suporte:

val camera = provider.bindToLifecycle(
    lifecycleOwner,
    CameraSelector.DEFAULT_BACK_CAMERA,
    preview,
    analysis,
)

val hasFlash = camera.cameraInfo.hasFlashUnit()

fun setTorch(enabled: Boolean) {
    if (hasFlash) {
        camera.cameraControl.enableTorch(enabled)
    }
}

Tenha alternativa manual

Câmera quebrada, lente suja, impressão ruim e aparelho sem serviço compatível acontecem. Para inventário e códigos numéricos, ofereça digitação manual. Um fallback bom reduz chamados de suporte.

Não prometa leitura impossível

Códigos pequenos, inclinados, refletivos, danificados ou muito densos podem falhar. Instrua a pessoa a aproximar, afastar, melhorar a luz ou centralizar a imagem sem culpá-la.

Performance e memória

O scanner roda continuamente, então pequenos desperdícios se repetem muitas vezes.

Boas práticas:

  • use STRATEGY_KEEP_ONLY_LATEST;
  • analise em executor dedicado;
  • restrinja formatos;
  • evite converter todo frame para Bitmap;
  • não copie arrays de pixels sem necessidade;
  • não faça chamadas de rede dentro do analyzer;
  • envie ao ViewModel somente resultados pequenos;
  • pause análise quando uma confirmação estiver aberta;
  • feche scanner e executor no ciclo de vida apropriado.

A regra “não faça rede no analyzer” é importante. Primeiro aceite e normalize o código; depois o ViewModel inicia a consulta. Assim, a câmera não fica acoplada à latência do backend.

Se o preview apresenta travamentos, use o Android Studio Profiler para investigar CPU e memória e o JankStats com Compose para localizar frames lentos da interface. Não conclua que o ML Kit é o problema sem medir o pipeline completo.

Privacidade e dados

A análise local reduz a necessidade de enviar imagens da câmera a um servidor. Ainda assim, o app precisa explicar o uso da câmera e tratar o valor lido de acordo com sua finalidade.

Evite:

  • salvar frames sem necessidade;
  • registrar tokens ou URLs completos em analytics;
  • enviar conteúdo bruto para logs de produção;
  • associar leitura a identidade quando isso não é necessário;
  • manter histórico indefinido por conveniência;
  • capturar imagem quando apenas o valor decodificado é suficiente.

Prefira telemetria agregada, como scan_success, invalid_format e duração da tentativa, sem incluir o conteúdo. Se o código carrega dado pessoal ou autenticação, aplique armazenamento e transporte compatíveis com o risco. O guia de LGPD para apps Android com Kotlin ajuda a transformar minimização e finalidade em decisões técnicas.

Como testar sem depender só da câmera real

A integração precisa de testes em camadas.

Teste validação como Kotlin puro

class ValidateBarcodeTest {

    @Test
    fun `aceita link do host oficial`() {
        val result = ValidateBarcode().invoke(
            "https://app.exemplo.com/check-in/abc123"
        )

        assertTrue(result is BarcodeValidation.Valid)
    }

    @Test
    fun `rejeita link de host desconhecido`() {
        val result = ValidateBarcode().invoke(
            "https://exemplo-malicioso.test/check-in/abc123"
        )

        assertTrue(result is BarcodeValidation.Invalid)
    }
}

Teste o controle de duplicidade com relógio injetável

@Test
fun `bloqueia o mesmo valor durante cooldown`() {
    var clock = 1_000L
    val guard = DuplicateGuard(
        cooldownMillis = 1_500L,
        now = { clock },
    )

    assertTrue(guard.shouldAccept("789123"))

    clock += 300L
    assertFalse(guard.shouldAccept("789123"))

    clock += 1_500L
    assertTrue(guard.shouldAccept("789123"))
}

Teste a UI com uma fonte fake

Compose não precisa inicializar câmera real para validar os estados Scanning, Found, Invalid e Error. Injete um controller fake que emite resultados conhecidos e confirme texto, botões e navegação.

Teste imagens conhecidas

Mantenha um conjunto pequeno de códigos gerados para teste, com formatos, tamanhos, rotações e contraste diferentes. Não use dados pessoais ou tokens reais nos fixtures.

Teste em aparelhos físicos

O emulador ajuda no fluxo básico, mas não representa foco, exposição, lente, rotação e performance de fabricantes diferentes. Valide pelo menos:

  • aparelho de entrada e intermediário;
  • baixa iluminação;
  • rotação de tela;
  • negação de permissão;
  • retorno após background;
  • código muito próximo e distante;
  • dois códigos no mesmo frame;
  • código inválido ou host não autorizado;
  • scanner aberto e fechado repetidamente;
  • operação offline quando o modelo deve estar disponível localmente.

Para testes de interface fora do preview, use a estratégia do guia de Espresso no Android com Kotlin.

Erros comuns

Esquecer imageProxy.close()

É o erro clássico: a análise funciona por alguns instantes e “congela”. Feche em todos os caminhos, preferencialmente no callback de conclusão.

Processar todos os frames em fila

A câmera produz mais imagens do que o detector precisa. Use KEEP_ONLY_LATEST e limite uma análise em andamento.

Criar scanner a cada frame

Instancie BarcodeScanner uma vez por ciclo de uso e reutilize. Criar objetos pesados repetidamente aumenta latência e pressão de memória.

Fazer navegação dentro do analyzer

O analyzer deve emitir um evento; ViewModel e UI decidem o efeito. Isso evita acoplamento a NavController, thread e lifecycle.

Aceitar qualquer URL

Decodificar não significa confiar. Valide esquema, host, tamanho e finalidade antes de abrir ou executar.

Disparar a mesma ação várias vezes

Use trava, cooldown ou confirmação. Um código parado diante da lente aparece em muitos frames.

Guardar a imagem sem necessidade

Para a maioria dos scanners, basta o valor. Capturar e armazenar o frame aumenta risco de privacidade e custo de armazenamento.

Testar apenas com QR Code perfeito no monitor

Inclua impressão amassada, pouca luz, reflexo e aparelhos reais. O caminho feliz não representa um depósito, balcão ou evento.

Checklist para produção

  • a câmera é solicitada somente quando o usuário abre o scanner;
  • existe explicação contextual e tratamento de negativa;
  • CameraX está vinculado ao lifecycle;
  • ImageAnalysis usa KEEP_ONLY_LATEST;
  • formatos aceitos foram restringidos;
  • cada ImageProxy é fechado em sucesso e falha;
  • somente uma análise fica ativa por vez;
  • resultados duplicados são bloqueados;
  • conteúdo é normalizado e validado antes da ação;
  • URLs aceitam somente hosts e esquemas previstos;
  • tokens sensíveis não aparecem em logs ou analytics;
  • há fallback manual quando o caso de uso permite;
  • lanterna e feedback respeitam capacidade e preferência;
  • scanner, analyzer e executor são liberados corretamente;
  • UI é testável com fonte fake;
  • validação foi testada com Kotlin puro;
  • aparelhos físicos e condições ruins foram incluídos na matriz;
  • comportamento offline do modelo escolhido foi verificado.

Perguntas frequentes

ML Kit precisa de internet para ler QR Code?

A leitura pode ocorrer no dispositivo, mas a disponibilidade inicial depende do artefato escolhido. Uma variante empacota o modelo no app; outra pode depender de download por serviços do Google. Verifique a documentação e teste a primeira execução offline antes de prometer funcionamento sem rede.

Preciso converter o frame em Bitmap?

Não para o fluxo comum com CameraX. Use InputImage.fromMediaImage com a rotação de ImageProxy. Converter todos os frames para Bitmap adiciona cópia, memória e latência desnecessárias.

Posso ler vários códigos no mesmo frame?

O scanner pode devolver uma lista. Seu produto deve decidir se aceita o primeiro, destaca todos ou exige que apenas um esteja visível. Para inventário em lote, ordenação e feedback visual precisam ser definidos para não registrar o item errado.

Como ler somente QR Code?

Configure BarcodeScannerOptions apenas com Barcode.FORMAT_QR_CODE. Restringir o formato torna a intenção explícita e evita interpretar outros códigos presentes no enquadramento.

CameraX funciona diretamente em Compose?

O estado e os controles podem ser Compose, mas o preview normalmente usa PreviewView dentro de AndroidView. Essa interoperabilidade é esperada e também aparece em componentes como players de vídeo.

Posso abrir automaticamente a URL encontrada?

Tecnicamente sim, mas não é uma boa regra geral. Valide o destino e mostre confirmação, principalmente para URLs externas, autenticação, pagamento ou ações irreversíveis.

O scanner substitui um leitor físico de código de barras?

Depende do volume e do ambiente. Câmera atende muitos fluxos ocasionais, mas operações intensivas podem exigir leitor dedicado por velocidade, ergonomia e resistência. Mantenha a validação desacoplada para aceitar ambas as entradas.

Conclusão e próximos passos

Um leitor confiável no Android nasce da combinação de responsabilidades pequenas: CameraX fornece preview e frames, ML Kit reconhece os códigos, o ViewModel aplica regras e Compose mostra estado e feedback. A implementação deixa de ser frágil quando você descarta frames antigos, fecha cada ImageProxy, restringe formatos e trata o valor lido como entrada não confiável.

Comece com um único caso de uso e um único formato. Meça tempo até a primeira leitura, teste duplicidade, valide condições ruins e ofereça alternativa manual. Só depois acrescente lanterna, leitura em lote, imagens da galeria ou integração de inventário.

Para continuar a trilha, leia CameraX com Kotlin e Compose, Photo Picker no Android, permissões modernas no Android e Android Studio Profiler. Como projeto de portfólio, um scanner com validação, testes, fallback e métricas demonstra muito mais maturidade que uma tela capaz apenas de imprimir rawValue no log.