plugin_scanner_qr
Plugin Flutter para leitura de QR Code no Android usando ML Kit e CameraX.
Dois modos:
| Modo | Como aparece | Use quando |
|---|---|---|
Leitura única — scanQRCode() |
Activity de tela cheia; lê um código e volta | Ler um QR pontualmente |
Leitura contínua — QrScannerPreview |
PlatformView embutida na árvore de widgets | Leitura o tempo todo, com UI Flutter por cima (overlay, diálogos, botões) |
Plataformas: Android apenas.
Instalação
dependencies:
plugin_scanner_qr: ^0.4.0
Permissões (Android)
O plugin já declara CAMERA e mescla no manifest do app — não é preciso repetir.
O plugin não solicita a permissão. Isso é deliberado: se o app também usa
permission_handler, dois solicitantes concorrentes causam
IllegalStateException: Can request only one set of permissions at a time.
Solicite no app e use hasCameraPermission() para consultar:
if (!await plugin.hasCameraPermission()) {
await Permission.camera.request(); // permission_handler
}
Se a permissão faltar, QrScannerPreview não fica preta em silêncio: emite um
QrScannerError com isPermissionDenied == true e renderiza o
permissionDeniedBuilder.
Uso rápido
Leitura única
import 'package:plugin_scanner_qr/plugin_scanner_qr.dart';
final plugin = PluginScannerQr();
final result = await plugin.scanQRCode();
print(result); // conteúdo do QR ou null se cancelado
Leitura contínua com preview embutido (recomendado)
O preview é um widget normal, então dá para desenhar qualquer coisa por cima num
Stack — inclusive showDialog.
class _MinhaTelaState extends State<MinhaTela> {
late final _controller = QrScannerController(
duplicateDebounce: const Duration(milliseconds: 1500),
initialFacing: QrCameraFacing.front,
);
@override
void initState() {
super.initState();
_controller.scans.listen(_onLeitura);
_controller.errors.listen((e) => print('erro: ${e.code}'));
}
Future<void> _onLeitura(QrScanResult r) async {
await _controller.pauseScanning(); // mantém a câmera aberta
await _processar(r.value);
await _controller.resumeScanning();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Stack(
children: [
QrScannerPreview(controller: _controller),
const Positioned(bottom: 24, left: 0, right: 0,
child: Text('Aproxime o crachá', textAlign: TextAlign.center)),
],
);
}
stop/start vs pauseScanning/resumeScanning
pauseScanning()/resumeScanning()— a câmera continua aberta, só a detecção para. É quase instantâneo. Use no padrão "leu → mostra diálogo → retoma".stop()/start()— fecha e reabre o dispositivo de câmera. Use ao navegar para outra tela ou quando o app vai para background.
Migrando do ai_barcode
ai_barcode |
aqui |
|---|---|
ScannerController(scannerResult: ...) |
QrScannerController + scans.listen(...) |
scannerViewCreated: |
QrScannerPreview(onViewCreated: ...) |
startCamera() / startCameraFront() |
start(facing: QrCameraFacing.back / .front) |
startCameraPreview() |
resumeScanning() |
stopCamera() |
stop() |
stopCameraPreview() |
pauseScanning() |
isStartCamera |
isRunning |
PlatformAiBarcodeScannerWidget(...) |
QrScannerPreview(controller: ...) |
Comandos chamados antes de a PlatformView existir são guardados e aplicados no
attach — não é preciso esperar por isAttached.
Leitura contínua em baixo nível (controle total)
// 1. Assine o stream ANTES de abrir a câmera
plugin.onBarcodeScanned.listen((result) {
print(result.value);
});
// 2. Abra a câmera
await plugin.startContinuousScan(
ContinuousScanOptions(
duplicateDebounce: Duration(milliseconds: 1500),
cooldown: Duration(seconds: 2),
),
);
// Controle manual:
await plugin.pauseScanning();
await plugin.resumeScanning();
await plugin.stopContinuousScan();
Removido na 0.2.0.
startContinuousScanagora responde com o erroUNSUPPORTED. O modo contínuo abria uma Activity de tela cheia que cobria toda a UI Flutter, deixando invisíveis overlays eshowDialog. UseQrScannerPreview+QrScannerController. Os métodos globaispauseScanning/resumeScanning/toggleFlash/switchCamera/stopContinuousScancontinuam funcionando, mas delegam para a últimaQrScannerPreviewcriada — prefira os métodos por instância do controller.
QrScannerController
| Membro | Descrição |
|---|---|
scans |
Stream<QrScanResult> com as leituras |
errors |
Stream<QrScannerError> (permissão, falha ao abrir a câmera) |
start({facing}) |
Abre a câmera. Idempotente. false se faltar permissão |
stop() |
Fecha o dispositivo de câmera |
pauseScanning() |
Desliga só a detecção, câmera segue aberta |
resumeScanning() |
Retoma a detecção (reabre a câmera se necessário) |
setFacing() / switchCamera() |
Frontal ↔ traseira, com o preview no ar |
setTorch(bool) |
Lanterna |
resetDebounce() |
Permite reler imediatamente o mesmo código |
setFocusMode(mode, {distance, interval}) |
Estratégia de foco (veja Calibração abaixo) |
setExposureCompensation(int) |
Compensação de exposição, em índices |
setLinearZoom(double) |
Zoom linear 0.0–1.0 |
getCameraCapabilities() |
O que o hardware suporta (foco manual, exposição, zoom…) |
isAttached / isRunning / isScanning |
Estado |
Calibração de câmera (kiosk/totem)
Novidades da 0.3.0, pensadas para leitura em ponto fixo (ex.: relógio de ponto) com muita luz ambiente e aparelhos de autofoco lento (ex.: Galaxy A03s). Tudo é opcional; sem configurar nada, o comportamento é o padrão da câmera.
final controller = QrScannerController(
// Resolução do frame analisado pelo ML Kit. Padrão: hd (1280x720).
// O default antigo do CameraX (sd, 640x480) rendia poucos pixels por módulo
// do QR — qualquer desfoque leve matava a leitura.
analysisResolution: QrAnalysisResolution.hd,
// continuous (padrão) | periodic | fixed
focusMode: QrFocusMode.fixed,
focusDistance: 4.0, // dioptrias = 1/metros → 4.0 ≈ 25 cm
);
| Modo de foco | O que faz | Use quando |
|---|---|---|
continuous |
Autofoco contínuo do CameraX (padrão) | Uso geral |
periodic |
Dispara foco no centro a cada autoFocusInterval (padrão 3 s) |
AF por contraste que "caça" e trava no plano errado |
fixed |
Desliga o AF e trava a lente em focusDistance dioptrias |
Totem: distância de leitura sempre igual — o foco nunca mais caça |
Referência rápida de dioptrias: 15 cm ≈ 6.7 · 25 cm ≈ 4.0 · 50 cm ≈ 2.0.
Se o aparelho não expõe foco manual, fixed cai silenciosamente em
continuous (nada quebra) e setFocusMode() retorna false. Descubra o que
o aparelho aceita com:
final caps = await controller.getCameraCapabilities();
print(caps); // focoManual, range de exposição, zoom, resolução negociada…
if (caps!.supportsManualFocus) {
await controller.setFocusMode(QrFocusMode.fixed, distance: 4.0);
}
Complementos:
setExposureCompensation(-4)— índices negativos escurecem; útil contra reflexo de QR exibido em tela de celular. Range e passo em EV vêm nas capabilities; o valor é ajustado ao range automaticamente.setLinearZoom(0.15)— QR ocupa mais pixels na distância típica do totem.- A resolução da análise é fixada na criação do preview (limitação do CameraX);
para trocar, recrie o widget. O CameraX negocia a resolução suportada mais
próxima — a efetiva aparece em
caps.analysisWidth/Height.
O example traz um painel "Calibração da câmera" com tudo isso em sliders, para calibrar no aparelho real sem recompilar.
QrScannerPreview
| Parâmetro | Padrão | Descrição |
|---|---|---|
controller |
— | Um controller por preview |
hybridComposition |
false |
false = TLHC (recomendado). Ligue só se houver artefatos |
autoPauseOnBackground |
true |
Fecha a câmera em background e reabre ao voltar |
onViewCreated |
— | Equivale ao scannerViewCreated do ai_barcode |
permissionDeniedBuilder |
— | UI quando falta permissão de câmera |
placeholder |
— | Exibido enquanto a câmera não abriu |
ContinuousScanOptions
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
duplicateDebounce |
Duration |
1500 ms |
Suprime reemissões do mesmo valor no nativo |
cooldown |
Duration |
Duration.zero |
Pausa automática após cada leitura |
useFrontCamera |
bool |
false |
Câmera frontal |
orientation |
String |
'portrait' |
'portrait' ou 'landscape' |
enableTorch |
bool |
false |
Lanterna |
showNativeFeedback |
bool |
false |
Toasts nativos de debug |
ContinuousQrScanner
| Membro | Descrição |
|---|---|
start() |
Solicita permissão e abre a câmera |
scans |
Stream<QrScanResult> com os resultados |
pause() |
Pausa temporariamente a detecção |
resume() |
Retoma após pausa |
toggleFlash() |
Liga/desliga a lanterna |
switchCamera() |
Alterna entre câmera frontal e traseira |
dispose() |
Encerra o scanner e fecha a câmera |
Leitura única com opções avançadas
final result = await plugin.scanQRCodeWithOptions(
useFrontCamera: false,
orientation: 'portrait',
enableTorch: false,
enableAutoFocus: true,
zoomLevel: 0.0,
);
Beep de confirmação
Beep nativo (ToneGenerator, os tons DTMF do sistema) — alto, instantâneo,
sem assets e sem dependência de áudio. Num kiosk, o operador depende do retorno
audível para saber se a batida passou; SystemSound.play do Flutter é discreto
demais e varia por fabricante.
final plugin = PluginScannerQr();
// Atalhos
await plugin.beepSuccess(); // tom de confirmação
await plugin.beepError(); // tom de erro, mais longo
// Ou parametrizado
await plugin.playBeep(
tone: QrBeepTone.doubleBeep, // beep | doubleBeep | confirm | deny | error | alert
duration: Duration(milliseconds: 300),
volume: 100, // 0–100, relativo ao volume do stream
stream: QrBeepStream.alarm, // music (padrão) | alarm | notification
);
- Tons curtos (
beep,confirm…) terminam sozinhos; contínuos (error) tocam porduration. stream: alarmusa o volume de alarme, independente do de mídia — num totem é mais difícil de silenciar por acidente.- Nunca lança: se o hardware de áudio recusar, retorna
false. - O volume final depende do volume do stream no aparelho — em kiosk, deixe o
volume do aparelho no máximo e ajuste pelo parâmetro
volume.
Utilitários
await plugin.isCameraAvailable(); // bool
await plugin.isFrontCameraAvailable(); // bool
await plugin.hasCameraPermission(); // bool — consulta pura, não solicita
Dependências nativas (Android)
- CameraX (
androidx.camera:camera-*1.3.4) - ML Kit Barcode Scanning (
com.google.mlkit:barcode-scanning17.2.0)
minSdk 21, compileSdk 35, Java/Kotlin jvmTarget 11.
Limitações conhecidas
- Apenas Android. iOS, Web e Desktop não são suportados.
- Em
scanQRCodeWithOptions, apenasuseFrontCamera,orientationeenableTorchchegam ao nativo. Os parâmetrosenableAutoFocus,zoomLevel,enableZoom,brightness,contrasteexposureCompensationsão aceitos e ignorados — ainda não foram conectados à API do CameraX. QrScanResult.operator ==compara apenasvalue, ignorandoscannedAt.- O preview usa
PreviewView.ImplementationMode.COMPATIBLE(TextureView).PERFORMANCE(SurfaceView) não é usável dentro de uma PlatformView. - Uma câmera física por vez: duas
QrScannerPreviewsimultâneas fazem a segunda emitirCAMERA_BIND_FAILED.
Estrutura do projeto
lib/
├── plugin_scanner_qr.dart # Fachada pública
├── plugin_scanner_qr_platform_interface.dart # Contrato da plataforma
├── plugin_scanner_qr_method_channel.dart # Implementação via channels
└── src/
├── qr_scanner_controller.dart # Controller do preview embutido
├── qr_scanner_preview.dart # Widget do preview (PlatformView)
├── qr_scan_result.dart # Modelo de resultado
├── continuous_scan_options.dart # Opções (legado)
└── continuous_qr_scanner.dart # Helper de alto nível (legado)
android/
└── src/main/kotlin/com/example/plugin_scanner_qr/
├── PluginScannerQrPlugin.kt # Registro, method channel, permissão
├── QrScannerCore.kt # CameraX + ML Kit, debounce/cooldown
├── QrPreviewView.kt # PlatformView + canais por instância
├── QrPreviewViewFactory.kt # PlatformViewFactory
├── PreviewLifecycleOwner.kt # LifecycleOwner por preview
├── PreviewRegistry.kt # Views vivas + espelho do host
└── MlKitScannerActivity.kt # Tela cheia, leitura única