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:
| Componente | Responsabilidade |
|---|---|
CameraX Preview | mostrar o enquadramento ao usuário |
CameraX ImageAnalysis | entregar frames para processamento |
| ML Kit Barcode Scanning | localizar e decodificar QR Codes e códigos de barras |
| ViewModel | controlar estado, repetição e efeitos de navegação |
| Jetpack Compose | renderizar 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
httpsquando 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;
-
ImageAnalysisusaKEEP_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.