zp_qr_scanner
A high-performance Flutter QR scanner plugin powered by native camera textures on iOS (AVFoundation) and Android (CameraX + bundled ML Kit), streamed into Flutter via TextureRegistry.
✨ Features
- 📱 iOS & Android:
AVFoundationon iOS,CameraX+ bundledML Kiton Android (no Google Play Services required). - ⚡ High Performance: Native camera texture streaming directly into Flutter via
Texture(textureId: ...), avoiding expensive platform view and image conversion overhead. - 🎯 Smooth Real-time Tracking: Automatically detects and tracks QR corners with smooth bounding box and scanning animations. The preview freezes on the exact frame the QR was detected in, so the corners always line up.
- 🔦 Torch & Focus Controls: Built-in support for toggling flashlight and manual tap-to-focus.
- 🔍 Zoom: Pinch-to-zoom in
QrScannerView, or set a zoom level such as1.0,2.0,3.0from code. - 📳 Haptic Feedback: Built-in haptic feedback on successful scans.
- 📱 Low Memory & Battery Efficient: Clean lifecycle management with automatic resume and pause.
🚀 Getting Started
1. Add dependency
Add zp_qr_scanner to your pubspec.yaml:
dependencies:
zp_qr_scanner: ^1.3.0
Or install it via CLI:
flutter pub add zp_qr_scanner
2. Platform Setup
iOS Setup
Add the camera permission description to your ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to scan QR codes and barcodes.</string>
Note: Requires iOS 15.5 or higher.
Android Setup
No extra setup is needed: the plugin declares the CAMERA permission and requests it at runtime when start() is called.
minSdkVersion21 or higher.- Your activity must be a
FlutterActivityorFlutterFragmentActivity(the default for Flutter apps). - The QR model is bundled (ML Kit), so scanning works without Google Play Services; it adds roughly 2–3 MB per ABI to the APK.
App size impact
Measured with ./.build/analyze-size.sh (Flutter 3.29.0, release build, empty app vs. app using QrScannerView):
| Platform | Added size |
|---|---|
| Android arm64-v8a | ~3.9 MB |
| Android armeabi-v7a | ~3.6 MB |
| iOS arm64 | ~0.2 MB |
On Android most of it is the bundled ML Kit barcode model; iOS uses the system AVFoundation framework.
📖 Usage
Basic Scanner View
Use QrScannerView to display a full-featured scanner screen:
import 'package:flutter/material.dart';
import 'package:zp_qr_scanner/zp_qr_scanner.dart';
class ScanPage extends StatelessWidget {
const ScanPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: QrScannerView(
onScanned: (String value) async {
// Handle scan result
await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Scanned Result'),
content: Text(value),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
// Once the Future finishes, scanner resumes automatically!
},
),
);
}
}
Scan frame position
The idle scan frame is centered by default. Use scanBoxAlignment to move it and scanBoxOffset to fine-tune it in logical pixels:
QrScannerView(
onScanned: _handleScanned,
scanBoxAlignment: const Alignment(0, -0.3), // a bit above center
scanBoxOffset: const Offset(0, 16), // then 16px down
)
The whole camera frame is still scanned, so a QR code outside the frame is detected too.
Zoom
Pinch with two fingers to zoom in and out (set enablePinchZoom: false to disable it). To offer fixed zoom levels, call ZpQrScanner.setZoom and listen to ZpQrScanner.zoom:
// e.g. from a "2x" button
await ZpQrScanner.setZoom(2.0);
ValueListenableBuilder<double>(
valueListenable: ZpQrScanner.zoom,
builder: (_, zoom, __) => Text('${zoom.toStringAsFixed(1)}x'),
)
1.0 is the main (wide) lens. The level is clamped to ZpQrScanner.minZoom..ZpQrScanner.maxZoom (available after the camera starts): minZoom is below 1.0 on devices with an ultra-wide lens, and maxZoom is capped at 10.0. On Android, and on iOS devices with a single wide lens, the camera may start slightly zoomed in (up to 2.0) to keep small QR codes in focus.
Scan control
Pause QR detection while keeping the camera preview running (e.g. while some other UI covers the camera), and recover a camera that got stuck:
// Pause detection; camera/preview keep running
await ZpQrScanner.stopScan();
// Resume detection
await ZpQrScanner.resumeScan();
ValueListenableBuilder<bool>(
valueListenable: ZpQrScanner.isScanning,
builder: (_, isScanning, __) => Text(isScanning ? 'Scanning' : 'Paused'),
)
// Recover a stuck camera: restarts the session only if it isn't
// actually showing a fresh frame anymore.
await ZpQrScanner.restart();
| Method | What it does |
|---|---|
stop() / resume() |
Full camera lifecycle: stop() releases the camera and texture; resume() only un-freezes the preview after a detected code — unrelated to pausing detection. |
stopScan() / resumeScan() |
Pauses/resumes QR detection only. The camera and preview keep running normally. |
restart() |
Recovery: checks whether the camera is actually showing a fresh frame, and only tears down and restarts the session if it isn't. A no-op (besides returning the current state) if everything is already fine. |
QrScannerView already reacts to isScanning and to texture changes from restart(), so these are safe to call even while a QrScannerView is on screen — including from outside the widget that created it.
restart() only recovers a camera that is already running (or was running and got stuck) — it won't turn the camera on by itself. Calling it before start(), or after stop(), throws a PlatformException(code: 'not_started'); calling it twice before the first call finishes may throw PlatformException(code: 'restart_in_progress') on Android. Call start() first if you want to begin a new session.
Low-level API (ZpQrScanner)
If you want custom UI or manual camera control:
// Start camera session and get texture metadata
final QrTexture texture = await ZpQrScanner.start();
print('Texture ID: ${texture.id}, Size: ${texture.size}');
// Listen to scanned results stream
final subscription = ZpQrScanner.results.listen((QrScanResult result) {
print('Scanned value: ${result.value}');
print('Corners: ${result.corners}');
});
// Toggle torch / flashlight
await ZpQrScanner.setTorch(true);
// Focus at a specific point (normalized 0.0 - 1.0)
await ZpQrScanner.focus(const Offset(0.5, 0.5));
// Zoom (clamped to ZpQrScanner.minZoom..ZpQrScanner.maxZoom)
final double applied = await ZpQrScanner.setZoom(2.0);
// Pause and resume
await ZpQrScanner.stop();
await ZpQrScanner.resume();
// Pause/resume detection only (camera keeps running), and recover a stuck camera
await ZpQrScanner.stopScan();
await ZpQrScanner.resumeScan();
await ZpQrScanner.restart();
🛠️ Example
Check out the complete sample project in the example/ directory.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.