smart_qr_scanner 1.6.0
smart_qr_scanner: ^1.6.0 copied to clipboard
A production-ready Flutter package for real-time QR code and barcode scanning using Google ML Kit with beautiful modern UI, animations, and advanced features.
smart_qr_scanner #

A production-ready Flutter package for real-time QR code and barcode scanning powered by Google ML Kit. Drop in one widget, get a full-featured scanner with a modern animated UI, built-in QR generator, gallery scan, persistent history, favorites, CSV export, URL handling, haptic feedback, and a clean developer API.
Features #
Scanning
- Real-time scanning — high-FPS camera stream with ML Kit processing
- Gallery scan — pick any image from the device gallery and extract barcodes
- 13 barcode formats — QR Code, Aztec, Codabar, Code 39/93/128, Data Matrix, EAN-8/13, ITF, PDF417, UPC-A/E
- Pinch-to-zoom — smooth gesture-based zoom, no slider UI clutter
- Smooth camera switch — front/back toggle with fade-in, no preview crash
- Instant camera open — pre-warm the controller before navigation so the preview is ready on landing
QR Code Generator
- Built-in QR generator —
QrGeneratorWidgetwith no external rendering dependency - 6 input types — URL, plain text, WiFi credentials, email, phone number, vCard contact
pretty_qr_coderendering —PrettyQrSquaresSymbolwith standard 4-module quiet zone ensures generated codes are reliably scannable by all QR readers- Accent-coloured finder patterns — eye regions use your accent color; data modules stay black
- Save to gallery — captures QR at 3× pixel ratio and saves via the
galpackage - Copy data — one-tap clipboard copy of the encoded string
UI & Animations
- Modern animated loader — glowing frame, sweep line, pulsing icon, animated dots
- Animated scan line — neon sweep with glow effect
- Pixel burst success animation — Paytm-style white blocks burst radially from the scan center;
ScanSuccessStyle.pixelBurst - Holographic QR overlay — scanned code materialises as a floating hologram with
Matrix43D tilt, float, entrance succession, and a cyan sweep line; rendered behind the pixel burst - Scan UI auto-hide — corner brackets, scan line, and hint text disappear the instant a code is detected during pixel burst mode and restore cleanly after
- Camera freeze on scan — camera pauses during the success animation and resumes on completion for a polished transition
- Dark scrim on scan success — semi-transparent overlay ensures white hologram modules are legible against any camera background
- Elastic check-mark ripple — default success animation (ripple rings + animated check-mark)
- Glassmorphism controls — flash, flip, gallery buttons with blur backdrop
- 3 built-in themes — Neon Cyan, Light, Minimal White + fully custom
- Theme picker — bottom sheet with animated selection rows
- Granular UI control — show/hide flash, gallery, flip, and menu buttons individually
- GlobalKey API — expose
showThemePicker()from any external button (e.g. AppBar) DuplicateToastOverlay— animated in-app toast for duplicate scan feedback
Data & Persistence
- Persistent history — scan history saved to
SharedPreferencesviaStorageService; survives app restarts - Favorites — bookmark any scan result; persisted across sessions via
FavoritesService - CSV export — export full scan history as a
.csvfile and share it viaHistoryExporter - JSON serialization —
SmartScanResult.toJson()/fromJson()for custom storage - Smart URL handler —
SmartUrlHandlerlaunches URLs, email clients, phone dialer, SMS, and geo coordinates
Feedback
- Haptic patterns per barcode type — distinct vibration patterns for URL, email, phone, WiFi, etc.
- Duplicate prevention — configurable time-window deduplication
Developer API
- Stream & Future APIs —
onScancallback,scanEventsstream, andscanOnce()Future - Lifecycle aware — auto-pause on background, resume on foreground
- Timeout handling — configurable scan deadline with callback
- Single & continuous modes — stop after first hit or keep scanning
- Permission handling — built-in request flow with settings deep-link
- Frame throttling — configurable skip count to save CPU/battery
Supported Formats #
| Format | Constant |
|---|---|
| QR Code | BarcodeFormat.qrCode |
| Aztec | BarcodeFormat.aztec |
| Codabar | BarcodeFormat.codabar |
| Code 39 | BarcodeFormat.code39 |
| Code 93 | BarcodeFormat.code93 |
| Code 128 | BarcodeFormat.code128 |
| Data Matrix | BarcodeFormat.dataMatrix |
| EAN-8 | BarcodeFormat.ean8 |
| EAN-13 | BarcodeFormat.ean13 |
| ITF | BarcodeFormat.itf |
| PDF417 | BarcodeFormat.pdf417 |
| UPC-A | BarcodeFormat.upca |
| UPC-E | BarcodeFormat.upce |
Installation #
dependencies:
smart_qr_scanner: ^1.6.0
flutter pub get
Platform Setup #
Android #
android/app/build.gradle
android {
defaultConfig {
minSdkVersion 21 // ML Kit requires 21+
}
}
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Gallery save: needed on Android 9 and below only -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:replace="android:maxSdkVersion" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application ...>
<!-- inside <application> -->
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui" />
</application>
</manifest>
iOS #
ios/Runner/Info.plist
<!-- Camera (required) -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required to scan QR codes and barcodes.</string>
<!-- Gallery scan (required) -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Required to pick images from your gallery for QR code scanning.</string>
<!-- Save generated QR to gallery -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Required to save generated QR codes to your photo library.</string>
<!-- Required by the camera plugin -->
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is not used but required by the camera plugin.</string>
ios/Podfile
platform :ios, '16.0' # google_mlkit_barcode_scanning 0.13+ requires iOS 16+
Quick Start #
Drop-in widget (simplest) #
import 'package:smart_qr_scanner/smart_qr_scanner.dart';
class ScanPage extends StatefulWidget {
const ScanPage({super.key});
@override
State<ScanPage> createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
late final SmartQrScannerController _controller;
@override
void initState() {
super.initState();
_controller = SmartQrScannerController(
config: const ScannerConfig(
scanMode: ScanMode.single,
enableVibration: true,
),
);
_controller.onScan = (result) {
print('Scanned: ${result.rawValue} (${result.formatName})');
};
_controller.initialize();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => SmartScannerWidget(
controller: _controller,
theme: ScannerTheme.light,
hintText: 'Point at a QR code or barcode',
);
}
Instant camera open (recommended) #
Pre-initialize the controller before pushing the scanner route so the camera warms up during the navigation transition and is ready the moment the screen lands:
void openScanner(BuildContext context) {
final controller = SmartQrScannerController(
config: const ScannerConfig(scanMode: ScanMode.single, enableVibration: true),
);
controller.initialize(); // fire immediately — overlaps with route animation
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScannerPage(controller: controller),
),
);
}
// In ScannerPage.initState():
// _controller = widget.controller; // use pre-warmed controller
// _controller.onScan = _onScan; // wire callbacks
// _controller.onTimeout = _onTimeout;
// _controller.onError = _onError;
// // do NOT call initialize() again
Future-based single scan #
final result = await _controller.scanOnce();
print(result.rawValue);
Gallery scan #
// Opens the device photo picker, scans the selected image,
// and fires onScan (or onError if no code is found).
await _controller.scanFromGallery();
QR Generator #
Drop QrGeneratorWidget anywhere to add a full-featured QR code creator:
QrGeneratorWidget(
accentColor: const Color(0xFF00BCD4), // finder pattern & button color
onGenerated: (data) => print('Generated: $data'),
)
Supported input types (selectable via chips inside the widget):
| Type | Encoded format |
|---|---|
| URL | Raw URL (auto-prefixes https://) |
| Text | Plain string |
| WiFi | WIFI:T:WPA;S:ssid;P:pass;; |
mailto:address |
|
| Phone | tel:number |
| Contact | vCard 3.0 (BEGIN:VCARD…END:VCARD) |
The rendered QR code uses PrettyQrSquaresSymbol with PrettyQrQuietZone.standard — standard sharp square modules with the required 4-module quiet zone — ensuring reliable scanning by all standard QR readers. Users can copy the data or save the QR image to the gallery.
Use QrView standalone #
// Renders a scannable QR code for any string
QrView(
data: 'https://flutter.dev',
size: 250,
eyeColor: Colors.teal, // finder pattern accent
dataColor: Colors.black, // data module color
background: Colors.white,
)
URL Handler #
SmartUrlHandler inspects a SmartScanResult and launches the appropriate system handler:
// Check if the result has a launchable action
if (SmartUrlHandler.canHandle(result)) {
// Returns a label like 'Open URL', 'Send Email', 'Call', 'Send SMS', 'Open Map'
final label = SmartUrlHandler.actionLabel(result);
await SmartUrlHandler.launch(result);
}
| Barcode type | Action |
|---|---|
| URL | Opens in system browser |
| Opens email composer | |
| Phone | Opens phone dialer |
| SMS | Opens SMS composer |
| Geo coordinates | Opens maps app |
Raw http/https |
Opens in system browser |
Favorites #
Persist favorite scan results across sessions:
final favService = FavoritesService();
// Check
final isFav = await favService.isFavorite(result.rawValue);
// Toggle (adds if not favorite, removes if favorite)
await favService.toggle(result.rawValue);
// Load all saved favorites
final favorites = await favService.loadAll();
FavoriteButton widget #
Drop-in animated bookmark button with a scale animation on toggle:
// In a list tile or AppBar action:
FavoriteButton.forValue(
result.rawValue,
activeColor: Colors.amber,
)
Scan History #
History is automatically persisted to SharedPreferences and restored on the next app launch.
// Access via the controller
final history = controller.history; // List<SmartScanResult>
// Clear
controller.clearHistory();
Export as CSV #
await HistoryExporter.exportCsv(controller.history);
// Generates a .csv file saved to the temporary directory; returns true on success
CSV columns: Timestamp, Format, Type, Raw Value, Display Value, Confidence
JSON Serialization #
SmartScanResult supports full round-trip JSON serialization for custom storage or API integration:
// Serialize
final json = result.toJson(); // Map<String, dynamic>
// Deserialize
final restored = SmartScanResult.fromJson(json);
All fields are preserved, including optional Rect, List<Offset>, and enum values.
Duplicate Toast #
Show an in-app toast when a duplicate barcode is scanned:
// Wrap your scanner screen in DuplicateToastOverlay:
DuplicateToastOverlay(
child: SmartScannerWidget(controller: _controller, ...),
)
// Trigger from anywhere in the subtree:
DuplicateToastOverlay.show(context, message: 'Already scanned!');
The toast fades in and auto-dismisses after 2 seconds.
Configuration #
SmartQrScannerController(
config: ScannerConfig(
// Scan behaviour
scanMode: ScanMode.single, // or ScanMode.continuous
cameraFacing: CameraFacing.back, // or CameraFacing.front
// Barcode formats (empty list = all 13 formats)
formats: [BarcodeFormat.qrCode, BarcodeFormat.ean13],
// Scan area (fraction of screen width/height)
scanAreaWidthFactor: 0.75,
scanAreaHeightFactor: 0.35,
// Camera
enableFlash: false,
enableAutoFocus: true,
// Feedback
enableVibration: true,
// Duplicate prevention
preventDuplicates: true,
duplicatePreventionWindow: Duration(seconds: 2),
// Performance
framesToSkip: 0, // 0 = every frame; 2 = every 3rd frame
// Timeout (null = no timeout)
scanTimeout: Duration(seconds: 30),
// History
enableScanHistory: true,
maxHistoryItems: 50,
// Analytics
onAnalyticsEvent: (value, format) => myAnalytics.track(value, format),
),
);
Controller API #
// Lifecycle
await controller.initialize();
await controller.dispose();
// Playback
controller.pause();
controller.resume();
// Camera
await controller.toggleFlash();
await controller.switchCamera();
await controller.setZoom(1.5); // pinch-to-zoom also handled by widget
// Gallery
await controller.scanFromGallery();
// State
controller.isInitialized // bool
controller.isPaused // bool
controller.isFlashOn // bool
controller.isSwitching // bool — true while swapping cameras
controller.permissionStatus // CameraPermissionStatus
controller.history // List<SmartScanResult>
controller.errorMessage // String?
controller.minZoom // double
controller.maxZoom // double
controller.currentZoom // double
// Callbacks
controller.onScan = (SmartScanResult result) { ... };
controller.onRawScan = (List<SmartScanResult> all) { ... };
controller.onTimeout = () { ... };
controller.onError = (String message) { ... };
// Streams
controller.scanEvents; // Stream<SmartScanResult> — deduplicated
controller.rawScanEvents; // Stream<List<SmartScanResult>> — every ML Kit batch
// Future API
final result = await controller.scanOnce();
// History
controller.clearHistory();
Widget Parameters #
SmartScannerWidget(
controller: controller,
// Theme (see Themes section below)
theme: ScannerTheme.light,
// Hint text shown below the scan area
hintText: 'Align code within the frame',
// Show/hide entire bottom control bar
showControls: true,
// Show/hide hint text
showHint: true,
// Show/hide individual bottom buttons
showFlash: true, // flash torch toggle
showGallery: true, // gallery image picker
showFlip: true, // front/back camera switch
// Show/hide the built-in 3-dots theme picker button.
// Set to false when you provide your own button via GlobalKey (see below).
showMenu: true,
// Called when user picks a theme from the bottom sheet
onThemeChanged: (ScannerTheme t) => setState(() => _theme = t),
// Success animation style
successStyle: ScanSuccessStyle.pixelBurst, // or ScanSuccessStyle.ripple
// Widget rendered BEHIND the success animation (e.g. holographic QR overlay).
// Receives the scan result; removed at the same time as the animation.
successOverlayBuilder: (result) => HolographicQrOverlay(rawValue: result.rawValue),
// Called after the success animation completes — use this for navigation
// instead of controller.onScan so the animation is not cut short.
onScanAnimationComplete: (result) => Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => ResultScreen(result: result)),
),
// Optional logo widget centered inside the pixel burst
successLogo: Image.asset('assets/logo.png', width: 90, height: 90),
successLogoSize: 100,
// Override the default loading / error screens
loadingWidget: MyLoadingScreen(),
errorWidget: MyErrorScreen(),
);
Holographic QR Overlay (example app widget) #
HolographicQrOverlay is a ready-to-use widget that renders the scanned QR code as a floating hologram with a cinematic entrance:
successOverlayBuilder: (result) => HolographicQrOverlay(
rawValue: result.rawValue,
// onDismiss: null → SmartScannerWidget controls removal
// onDismiss: () {} → widget self-dismisses after autoDismissAfter
),
| Property | Default | Description |
|---|---|---|
rawValue |
required | QR data to encode |
onDismiss |
null |
Removal callback; null = parent controls lifecycle |
autoDismissAfter |
2400 ms |
Auto-dismiss delay when onDismiss is set |
Entrance sequence (900 ms, TweenSequence):
| Phase | Scale | Opacity | Duration |
|---|---|---|---|
| Burst | 0.05× → 1.18× |
0 → 1 |
60 % |
| Overshoot | 1.18× → 0.94× |
1 |
20 % |
| Settle | 0.94× → 1.0× |
1 |
20 % |
Continuous animations after entrance:
- 3D tilt —
Matrix4perspective,rotateX±0.30 rad +rotateY±0.22 rad, 2200 ms loop - Float — ±6 px vertical, 2800 ms sine
- Cyan sweep line — isolated
CustomPainter, repaints only the scan line (1600 ms repeat)
Themes #
Built-in presets #
ScannerTheme.neon // Cyan glow, dark overlay (default)
ScannerTheme.light // White corners, sky-blue scan line, soft overlay
ScannerTheme.minimal // Pure white corners, minimal overlay
Custom theme #
ScannerTheme(
overlayColor: Color(0xAA000000),
borderColor: Color(0xFF00E5FF),
borderRadius: 16.0,
borderStrokeWidth: 3.5,
cornerLength: 28.0,
scanLineColor: Color(0xFF00E5FF),
scanLineHeight: 2.5,
scanLineAnimationDuration: Duration(milliseconds: 1800),
glassTintColor: Color(0x22FFFFFF),
glassBlurSigma: 12.0,
buttonColor: Color(0x44FFFFFF),
buttonIconColor: Colors.white,
successColor: Color(0xFF00E676),
hintTextStyle: TextStyle(color: Colors.white, fontSize: 14),
)
// Copy an existing preset and override only what you need:
ScannerTheme.neon.copyWith(borderColor: Colors.orange)
Theme picker in a custom AppBar #
final _scannerKey = GlobalKey<SmartScannerWidgetState>();
AppBar(
actions: [
IconButton(
icon: const Icon(Icons.more_vert_rounded, color: Colors.white),
onPressed: () => _scannerKey.currentState?.showThemePicker(),
),
],
)
SmartScannerWidget(
key: _scannerKey,
controller: _controller,
showMenu: false,
onThemeChanged: (t) => setState(() => _theme = t),
)
Scan Result #
SmartScanResult {
String rawValue; // Raw barcode string
String? displayValue; // Human-friendly value (formatted URL, phone, etc.)
BarcodeFormat format; // e.g. BarcodeFormat.qrCode
BarcodeType type; // e.g. BarcodeType.url
DateTime timestamp;
ScanResultType resultType; // success | duplicate | error | timeout
Rect? boundingBox;
List<Offset>? cornerPoints;
double? confidence; // 0.0 – 1.0
Map<String, dynamic> metadata; // structured data: email, phone, url, wifi …
// Helpers
String formatName; // 'QR Code', 'EAN-13', …
String typeName; // 'URL', 'Email', …
bool isSuccess;
// Serialization
Map<String, dynamic> toJson();
factory SmartScanResult.fromJson(Map<String, dynamic> json);
}
Performance Tips #
| Goal | Config / Approach |
|---|---|
| Instant camera open | Pre-create controller and call initialize() before Navigator.push |
| Reduce CPU on slow devices | framesToSkip: 2 |
| Faster cold start, lighter memory | enableScanHistory: false |
| Only need QR codes | formats: [BarcodeFormat.qrCode] |
| Prevent re-scan noise | duplicatePreventionWindow: Duration(seconds: 3) |
| Stop after first scan | scanMode: ScanMode.single |
| Smooth pixel burst | saveLayer is not used — all blocks drawn directly via canvas.drawRect; single Paint object reused across all 260 blocks per frame |
| Smooth hologram | Sweep line isolated in its own AnimatedBuilder; card tree never rebuilt by tilt/float ticks; RepaintBoundary wraps both burst and hologram |
Troubleshooting #
Scanner shows "iOS Simulator" screen instead of camera
iOS Simulator has no physical camera hardware. availableCameras() returns an empty list, so the package shows a friendly informational screen instead of a red error. Run the app on a real iPhone or iPad to use live scanning. Gallery scan (scanFromGallery()) and QR generation still work in the Simulator.
Camera permission denied on iOS
Add NSCameraUsageDescription to ios/Runner/Info.plist.
Gallery picker crashes on iOS
Add NSPhotoLibraryUsageDescription to ios/Runner/Info.plist.
Cannot save QR to gallery on iOS
Add NSPhotoLibraryAddUsageDescription to ios/Runner/Info.plist.
ML Kit models not downloading on Android
Add <meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="barcode_ui"/> inside <application> in AndroidManifest.xml.
Manifest merger conflict for WRITE_EXTERNAL_STORAGE
Add xmlns:tools to the root <manifest> tag and tools:replace="android:maxSdkVersion" to the <uses-permission> element. See the Platform Setup section above.
Black camera preview on open
Pre-warm the controller before Navigator.push (see Instant camera open above). Always call controller.dispose() inside your widget's dispose().
Generated QR code not scannable
QrView uses PrettyQrSquaresSymbol with PrettyQrQuietZone.standard — standard square modules and a 4-module quiet zone — which is reliably read by all QR apps. Ensure the widget is rendered against a white background so the quiet zone has sufficient contrast.
buildPreview() on disposed CameraController crash
Handled automatically via the isSwitching flag — the widget renders a black placeholder while the old controller is disposed.
Slow detection
Increase framesToSkip or restrict formats to only what you need.
No result from gallery image
The image must contain a clear, unobstructed barcode. Blurry, rotated, or very small codes may not be detected. onError is called with a descriptive message if nothing is found.
License #
MIT License — Copyright (c) 2026 Sanjay Sharma