plugin_scanner_qr 0.1.0
plugin_scanner_qr: ^0.1.0 copied to clipboard
Plugin Flutter para leitura de QR Code no Android com suporte a modo único e modo contínuo, usando ML Kit e CameraX. Inclui debounce nativo, cooldown configurável e helper Dart de alto nível (Continuo [...]
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:plugin_scanner_qr/plugin_scanner_qr.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// ── Estado geral ────────────────────────────────────────────────────────────
String _platformVersion = 'Carregando…';
String _qrCodeResult = 'Nenhum QR code escaneado ainda';
bool _isLoading = false;
// ── Opções de câmera (modo único e contínuo) ─────────────────────────────────
bool _useFrontCamera = false;
bool _enableTorch = false;
String _orientation = 'portrait';
bool _enableAutoFocus = true;
double _zoomLevel = 0.0;
bool _enableZoom = true;
double _brightness = 0.5;
double _contrast = 0.5;
double _exposureCompensation = 0.0;
// ── Opções exclusivas do modo contínuo ──────────────────────────────────────
int _cooldownSeconds = 2;
// ── Leitura contínua ────────────────────────────────────────────────────────
final _plugin = PluginScannerQr();
ContinuousQrScanner? _scanner;
bool _continuousRunning = false;
@override
void initState() {
super.initState();
_initPlatformState();
}
Future<void> _initPlatformState() async {
String version;
try {
version = await _plugin.getPlatformVersion() ?? 'Versão desconhecida';
} on PlatformException {
version = 'Falha ao obter versão da plataforma';
}
if (!mounted) return;
setState(() => _platformVersion = version);
}
// ── Leitura única ────────────────────────────────────────────────────────────
Future<void> _scanQRCode() async {
setState(() => _isLoading = true);
try {
if (!await _plugin.isCameraAvailable()) {
_showSnackBar('Câmera não disponível no dispositivo');
return;
}
if (_useFrontCamera && !await _plugin.isFrontCameraAvailable()) {
_showSnackBar('Câmera frontal não disponível');
return;
}
if (!await _plugin.requestCameraPermission()) {
_showSnackBar('Permissão de câmera necessária');
return;
}
final result = await _plugin.scanQRCodeWithOptions(
useFrontCamera: _useFrontCamera,
orientation: _orientation,
enableTorch: _enableTorch,
enableAutoFocus: _enableAutoFocus,
zoomLevel: _zoomLevel,
enableZoom: _enableZoom,
brightness: _brightness,
contrast: _contrast,
exposureCompensation: _exposureCompensation,
);
setState(() {
_qrCodeResult =
result ?? 'Escaneamento cancelado pelo usuário';
});
} on PlatformException catch (e) {
setState(() => _qrCodeResult = 'Erro: ${e.message}');
} finally {
setState(() => _isLoading = false);
}
}
// ── Leitura contínua ─────────────────────────────────────────────────────────
Future<void> _startContinuousScan() async {
if (_continuousRunning) return;
setState(() => _isLoading = true);
final scanner = ContinuousQrScanner(
options: ContinuousScanOptions(
useFrontCamera: _useFrontCamera,
orientation: _orientation,
enableTorch: _enableTorch,
cooldown: Duration(seconds: _cooldownSeconds),
),
);
try {
await scanner.start();
} on Exception catch (e) {
setState(() {
_isLoading = false;
_qrCodeResult = 'Erro ao iniciar scanner: $e';
});
await scanner.dispose();
return;
}
_scanner = scanner;
setState(() {
_isLoading = false;
_continuousRunning = true;
});
scanner.scans.listen(
(result) {
if (!mounted) return;
setState(() => _qrCodeResult = result.value);
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(
content: Text('QR lido: ${result.value}'),
duration: const Duration(seconds: 2),
),
);
},
onDone: () {
if (mounted) setState(() => _continuousRunning = false);
},
);
}
Future<void> _stopContinuousScan() async {
await _scanner?.dispose();
_scanner = null;
if (mounted) setState(() => _continuousRunning = false);
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
void dispose() {
_scanner?.dispose();
super.dispose();
}
// ── UI ───────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Scanner QR Code'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
const Icon(Icons.qr_code_scanner, size: 80, color: Colors.blue),
const SizedBox(height: 8),
Text(
'Plugin Scanner QR Code',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
Text(
'Plataforma: $_platformVersion',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 16),
// ── Configurações de câmera ──────────────────────────────────────
_SectionCard(
title: 'Configurações da Câmera',
children: [
SwitchListTile(
title: const Text('Câmera frontal'),
value: _useFrontCamera,
onChanged: (v) => setState(() => _useFrontCamera = v),
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: const Text('Flash'),
value: _enableTorch,
onChanged: (v) => setState(() => _enableTorch = v),
contentPadding: EdgeInsets.zero,
),
Row(
children: [
const Text('Orientação: '),
const SizedBox(width: 8),
DropdownButton<String>(
value: _orientation,
items: const [
DropdownMenuItem(
value: 'portrait',
child: Text('Vertical'),
),
DropdownMenuItem(
value: 'landscape',
child: Text('Horizontal'),
),
],
onChanged: (v) =>
setState(() => _orientation = v ?? 'portrait'),
),
],
),
SwitchListTile(
title: const Text('Foco automático'),
value: _enableAutoFocus,
onChanged: (v) => setState(() => _enableAutoFocus = v),
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: const Text('Zoom manual'),
value: _enableZoom,
onChanged: (v) => setState(() => _enableZoom = v),
contentPadding: EdgeInsets.zero,
),
if (_enableZoom)
_LabeledSlider(
label: 'Zoom',
value: _zoomLevel,
min: 0,
max: 1,
divisions: 10,
display: '${(_zoomLevel * 100).round()}%',
onChanged: (v) => setState(() => _zoomLevel = v),
),
_LabeledSlider(
label: 'Brilho',
value: _brightness,
min: 0,
max: 1,
divisions: 10,
display: '${(_brightness * 100).round()}%',
onChanged: (v) => setState(() => _brightness = v),
),
_LabeledSlider(
label: 'Contraste',
value: _contrast,
min: 0,
max: 1,
divisions: 10,
display: '${(_contrast * 100).round()}%',
onChanged: (v) => setState(() => _contrast = v),
),
_LabeledSlider(
label: 'Exposição',
value: _exposureCompensation,
min: -2,
max: 2,
divisions: 20,
display: _exposureCompensation.toStringAsFixed(1),
onChanged: (v) =>
setState(() => _exposureCompensation = v),
),
],
),
const SizedBox(height: 12),
// ── Opções do modo contínuo ──────────────────────────────────────
_SectionCard(
title: 'Modo Contínuo',
children: [
_LabeledSlider(
label: 'Cooldown após leitura',
value: _cooldownSeconds.toDouble(),
min: 0,
max: 10,
divisions: 10,
display: _cooldownSeconds == 0
? 'sem pausa'
: '${_cooldownSeconds}s',
onChanged: (v) =>
setState(() => _cooldownSeconds = v.round()),
),
const Text(
'Tempo que o scanner pausa automaticamente após ler um QR.\n'
'Use 0 para leitura ininterrupta.',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
const SizedBox(height: 16),
// ── Botões ───────────────────────────────────────────────────────
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed:
(_isLoading || _continuousRunning) ? null : _scanQRCode,
icon: _isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.qr_code_scanner),
label: const Text('Leitura única'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isLoading
? null
: (_continuousRunning
? _stopContinuousScan
: _startContinuousScan),
icon: _isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Icon(
_continuousRunning
? Icons.stop
: Icons.camera_alt,
),
label: Text(
_continuousRunning ? 'Parar' : 'Leitura contínua',
),
style: ElevatedButton.styleFrom(
backgroundColor:
_continuousRunning ? Colors.red : Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
const SizedBox(height: 16),
// ── Resultado ────────────────────────────────────────────────────
_SectionCard(
title: 'Resultado',
children: [
SelectableText(
_qrCodeResult,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
],
),
),
),
);
}
}
// ── Widgets auxiliares ────────────────────────────────────────────────────────
class _SectionCard extends StatelessWidget {
final String title;
final List<Widget> children;
const _SectionCard({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...children,
],
),
);
}
}
class _LabeledSlider extends StatelessWidget {
final String label;
final double value;
final double min;
final double max;
final int divisions;
final String display;
final ValueChanged<double> onChanged;
const _LabeledSlider({
required this.label,
required this.value,
required this.min,
required this.max,
required this.divisions,
required this.display,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(width: 90, child: Text(label)),
Expanded(
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
label: display,
onChanged: onChanged,
),
),
SizedBox(width: 50, child: Text(display, textAlign: TextAlign.end)),
],
);
}
}