Barcode & QR-Scanner Apps 2025: Von Inventur bis Ticket-System
Barcode- und QR-Scanner-Apps sind die Arbeitstiere der Digitalisierung: Inventur, Wareneingang, Ticket-Scanning, Asset-Tracking – überall wo physische Objekte erfasst werden müssen. Aber: Camera-Permissions sind tricky, Performance ist kritisch (langsames Scannen nervt!), und Offline-Fähigkeit ist für Business-Apps Pflicht.
In diesem Guide zeige ich dir:
- Barcode-Typen: EAN-13, QR, Code 128, Data Matrix – wann welcher?
- Camera-Permissions: iOS vs. Android (User sagt Nein → Fallback?)
- Performance: Scan-Speed, Auto-Focus, Torch, Duplicate-Prevention
- Real Case: Inventur-Scanner App mit SQLite, CSV-Export, Simulationsmodus
- Implementation: mobile_scanner Package (Flutter) + Code-Beispiele
- Kosten & Timeline: 12k-50k, 1,5-4 Monate
TL;DR – Die Essenz
| Aspekt | Zusammenfassung |
|---|---|
| Barcode-Typen | EAN-13 (Retail), QR (URLs/VCards), Code 128 (Logistik), Data Matrix (Industrie) |
| Camera-Permissions | Android: Manifest + Runtime. iOS: Info.plist + NSCameraUsageDescription. Fallback: Manuelle Eingabe/CSV-Import |
| Performance | Auto-Focus, Torch für schlechtes Licht, Duplicate-Prevention (lastScannedCode), Debouncing |
| Offline-Scan | SQLite für lokale Speicherung, Sync bei Netzverbindung, CSV-Export |
| Kosten | 12-18k (Basic Scanner), 25-35k (Inventur mit CRUD), 40-50k (Enterprise mit Backend) |
| Timeline | 1,5-2 Monate (Basic), 2-3 Monate (Inventur), 3-4 Monate (Enterprise) |
💡 Wann Scanner-App statt Web-App?
- Native App: Schnellerer Kamera-Zugriff, Offline-Funktionalität, bessere Performance
- Web-App (PWA): Wenn Budget <10k, aber: Kamera-Zugriff langsamer, Offline eingeschränkt
Hinweis: Alle Preise netto.
1. Barcode-Grundlagen: 1D vs. 2D
1.1 Barcode-Typen
1D-Barcodes (Strichcodes):
- Lesen: Von links nach rechts, Striche + Abstände kodieren Daten
- Datenkapazität: 20-25 Zeichen (begrenzt!)
- Scan-Distanz: Bis 1m (gute Bedingungen)
2D-Barcodes (Matrix-Codes):
- Lesen: In X- und Y-Richtung, mehr Daten auf kleiner Fläche
- Datenkapazität: Bis 4.296 Zeichen (QR-Code)
- Fehlerkorrektur: Reed-Solomon (beschädigte Codes lesbar)
1.2 Häufige Barcode-Typen
| Typ | Dimension | Datenkapazität | Typische Nutzung |
|---|---|---|---|
| EAN-13 | 1D | 13 Ziffern | Retail (Produkte im Supermarkt) |
| EAN-8 | 1D | 8 Ziffern | Kleine Produkte (Zigaretten, Süßigkeiten) |
| Code 128 | 1D | Variable | Logistik, Versand (Post, DHL) |
| Code 39 | 1D | Variable | Industrie, Lagerverwaltung (älterer Standard) |
| QR-Code | 2D | Bis 4.296 Zeichen | URLs, VCards, Tickets, Menüs |
| Data Matrix | 2D | Bis 3.116 Zeichen | Industrie (Klein-Teile, Elektronik, Pharma) |
| PDF417 | 2D | Bis 1.800 Zeichen | Reisepässe, Führerscheine, Boarding-Pässe |
| Aztec Code | 2D | Bis 3.067 Zeichen | Transport-Tickets (Bahn, Bus) |
💡 Empfehlung:
- Retail/Inventur: EAN-13 (Produkte haben meist EAN)
- Logistik: Code 128 (Versandlabels)
- Flexible Daten (URLs, Text): QR-Code (bis 4.296 Zeichen!)
- Klein-Teile (Elektronik): Data Matrix (kompakt, auch auf 5mm lesbar)
2. Camera-Permissions: Android vs. iOS
2.1 Android-Permissions
Manifest (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
Runtime-Permission (Flutter):
import 'package:permission_handler/permission_handler.dart';
Future<bool> requestCameraPermission() async {
final status = await Permission.camera.request();
if (status.isGranted) {
return true;
} else if (status.isDenied) {
// User hat abgelehnt
showPermissionDialog();
return false;
} else if (status.isPermanentlyDenied) {
// User hat "Nicht mehr fragen" aktiviert
openAppSettings();
return false;
}
return false;
}
⚠️ Android 11+ (API 30): Nach 2× “Nein” zeigt Android keine Dialoge mehr → isPermanentlyDenied → App muss zu Settings leiten (openAppSettings()).
2.2 iOS-Permissions
Info.plist (ios/Runner/Info.plist):
<key>NSCameraUsageDescription</key>
<string>Wir benötigen Kamera-Zugriff für Barcode-Scanning</string>
Runtime-Permission (Flutter - automatisch via mobile_scanner):
// iOS: Permission-Dialog wird automatisch gezeigt beim ersten Camera-Zugriff
// Kein manuelles Request nötig (anders als Android!)
if (!await isPermissionGranted()) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text("Kamera-Zugriff verweigert"),
content: Text(
"Bitte erlauben Sie Kamera-Zugriff in den Einstellungen:\n\n"
"Einstellungen > [App-Name] > Kamera"
),
actions: [
TextButton(
onPressed: () => openAppSettings(),
child: Text("Zu Einstellungen"),
),
],
),
);
}
⚠️ iOS-Besonderheit: Wenn User Permission verweigert → App muss neu gestartet werden nach Settings-Änderung (iOS cached Permission-Status seit iOS 15+!).
2.3 Permission-Fallback
Best Practice: Simulationsmodus oder CSV-Import anbieten, wenn User Camera-Permission verweigert (oder dauerhaft blockiert).
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: Permission.camera.isGranted,
builder: (context, snapshot) {
if (snapshot.data == true) {
return CameraScannerWidget();
} else {
return ManualInputWidget(); // Fallback: Barcode manuell eingeben oder CSV importieren
}
},
);
}
Fallback-Optionen:
- Manuelle Eingabe: User tippt Barcode ab (z.B. EAN-13 von Produkt)
- CSV-Import: Bulk-Upload von Barcodes (Excel → CSV → Import)
- Settings-Link: Button “Kamera-Zugriff erlauben” →
openAppSettings()
3. Performance-Optimierung
3.1 Scan-Speed
Problem: Langsames Scannen nervt User (>3 Sekunden = zu langsam).
Optimierungen:
- Auto-Focus aktivieren (mobile_scanner macht das automatisch)
- Frame-Rate limitieren (30 FPS reichen, spart CPU)
- Scan-Bereich einschränken (Region of Interest / ROI → nur Mitte analysieren)
- Exposure Lock nach erstem Fokus (verhindert Über-/Unterbelichtung)
- Torch (Taschenlampe) bei schlechtem Licht
UX-Tipps für besseres Scannen:
- Schräg halten bei glänzenden Folien (reduziert Reflexionen)
- Torch zuerst kurz aus, dann an (reduziert Blendflecken bei Spiegelungen)
- Abstand variieren (15-30 cm optimal für EAN-13)
Code-Beispiel (mobile_scanner):
MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal, // normal = 30 FPS, noDuplicates = nur 1x pro Code
facing: CameraFacing.back,
torchEnabled: false,
);
MobileScanner(
controller: controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null) {
print("Gescannt: ${barcode.rawValue}");
}
}
},
);
3.2 Torch (Taschenlampe) für schlechtes Licht
Use-Case: Lagerhallen, Keller, dunkle Räume → Torch aktivieren = 80% schnelleres Scannen.
IconButton(
icon: Icon(isTorchOn ? Icons.flashlight_off : Icons.flashlight_on),
onPressed: () {
controller.toggleTorch();
setState(() {
isTorchOn = !isTorchOn;
});
},
);
3.3 Duplicate-Prevention
Problem: Barcode wird 10x pro Sekunde gescannt → 10 Einträge in DB!
Lösung 1 (UI-seitig): lastScannedCode + Timeout (500ms).
String? _lastScannedCode;
DateTime? _lastScanTime;
void _processScan(String barcode) {
// Duplicate-Prevention: Gleicher Code innerhalb 500ms = ignorieren
if (_lastScannedCode == barcode &&
_lastScanTime != null &&
DateTime.now().difference(_lastScanTime!) < Duration(milliseconds: 500)) {
return; // Duplikat, ignorieren
}
_lastScannedCode = barcode;
_lastScanTime = DateTime.now();
// Scan verarbeiten (DB-Eintrag, etc.)
_saveToDatabase(barcode);
}
Lösung 2 (DB-seitig): UNIQUE-Constraint für Idempotenz (schützt vor Race-Conditions).
CREATE TABLE scans (
scan_uuid TEXT PRIMARY KEY, -- UUID pro Scan
barcode TEXT NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(barcode, timestamp/60000) -- Nur 1 Scan pro Barcode pro Minute
);
💡 Best Practice: Beide Lösungen kombinieren (UI + DB-Constraint).
3.4 Scan-Overlay (Rahmen zeigen)
UX-Tipp: Zeige User einen Scan-Rahmen, damit er weiß, wo er Barcode hinhalten muss.
Stack(
children: [
MobileScanner(...),
Positioned.fill(
child: CustomPaint(
painter: ScannerOverlayPainter(),
),
),
],
);
class ScannerOverlayPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black54
..style = PaintingStyle.fill;
// Schwarzer Overlay mit transparentem Rechteck in der Mitte
final holePath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
..addRRect(RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width * 0.7,
height: size.height * 0.4,
),
Radius.circular(16),
))
..fillType = PathFillType.evenOdd;
canvas.drawPath(holePath, paint);
// Rahmen zeichnen
final framePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width * 0.7,
height: size.height * 0.4,
),
Radius.circular(16),
),
framePaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
3.5 Low-Quality-Barcodes (beschädigte/verschmierte Codes)
Praxis-Problem: Beschädigte Barcodes, verschmierte Labels, schlechter Druck → Scanner versagt.
Lösungen:
- Torch aktivieren (bessere Beleuchtung = höhere Erfolgsrate)
- Mehrfach-Scans (wenn 1× fehlschlägt → automatisch nochmal probieren)
- Abstand variieren (näher/weiter weg = andere Fokus-Distanz)
- Manueller Fallback (User kann Barcode abtippen)
Code-Beispiel (Retry-Logic):
int _scanAttempts = 0;
final int _maxAttempts = 3;
void _processScan(String barcode) {
if (barcode.isEmpty || barcode.length < 8) {
_scanAttempts++;
if (_scanAttempts >= _maxAttempts) {
// Nach 3 Fehlversuchen → Manuellen Input anbieten
_showManualInputDialog();
_scanAttempts = 0;
}
return;
}
_scanAttempts = 0; // Erfolgreicher Scan → Reset
_saveToDatabase(barcode);
}
💡 UX-Tipp: Zeige User Fortschritt bei Retry (“Versuch 2/3…“).
4. Real Case: Inventur-Scanner App
Use-Case: Einzelhändler, Lager, Werkstätten nutzen Inventur-Scanner für jährliche Bestandsaufnahme. Mitarbeiter scannen Produkte (EAN-13), erfassen Menge + Notizen, exportieren CSV für Buchhaltung.
4.1 Funktionen
Kerntechnologie:
- Barcode-Scanning: EAN-13, QR, Code 128 (mobile_scanner Package)
- Offline-First: SQLite für lokale Speicherung (Hive oder Drift)
- Item-Management: Create/Update Item (Barcode, Name, Menge, Notiz)
- CSV-Export: Export als CSV für Excel/Buchhaltung
- Simulationsmodus: Barcode manuell eingeben (für Screencasts ohne Kamera!)
- Torch-Support: Taschenlampe für schlechtes Licht
Architektur:
[Camera-Scan] → [Barcode erkannt] → [lastScannedCode Check]
→ [Item aus DB laden] → [Menge +/- anpassen] → [SQLite Update]
→ [CSV-Export (optional)] → [Teilen per E-Mail/Cloud]
4.2 Code-Einblicke (Inventur-Scanner)
Scanner-Screen mit Simulationsmodus:
class ScannerScreen extends ConsumerStatefulWidget {
const ScannerScreen({super.key});
@override
ConsumerState<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends ConsumerState<ScannerScreen> {
bool _isProcessing = false;
String? _lastScannedCode;
@override
Widget build(BuildContext context) {
final isSimulation = ref.watch(simulationModeProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Scanner'),
actions: [
IconButton(
icon: const Icon(Icons.flashlight_on),
onPressed: () {
if (!isSimulation) {
ref.read(scannerControllerProvider).toggleTorch();
}
},
),
],
),
body: isSimulation ? _buildSimulationMode() : _buildCameraMode(),
);
}
Widget _buildSimulationMode() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Card(
color: Colors.orange.shade50,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Icon(Icons.developer_mode, size: 48, color: Colors.orange),
SizedBox(height: 8),
Text(
'Simulationsmodus aktiv',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Geben Sie einen Barcode manuell ein'),
],
),
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () => _showSimulationDialog(),
icon: const Icon(Icons.qr_code),
label: const Text('Barcode eingeben'),
),
],
),
);
}
Widget _buildCameraMode() {
return Stack(
children: [
MobileScanner(
controller: ref.read(scannerControllerProvider),
onDetect: (capture) {
if (!_isProcessing) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null &&
barcode.rawValue != _lastScannedCode) {
_lastScannedCode = barcode.rawValue;
_processScan(barcode.rawValue!);
break;
}
}
}
},
),
Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
color: Colors.black54,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Richten Sie die Kamera auf einen Barcode',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
),
],
);
}
void _showSimulationDialog() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Barcode eingeben'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Barcode',
hintText: 'z.B. 4012345678901',
),
autofocus: true,
keyboardType: TextInputType.text,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
ElevatedButton(
onPressed: () {
if (controller.text.isNotEmpty) {
Navigator.pop(context);
_processScan(controller.text);
}
},
child: const Text('Scannen'),
),
],
),
);
}
void _processScan(String barcode) {
if (_isProcessing) return;
setState(() {
_isProcessing = true;
});
_showItemBottomSheet(barcode);
}
void _showItemBottomSheet(String barcode) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => _ItemBottomSheet(
barcode: barcode,
onDone: () {
setState(() {
_isProcessing = false;
_lastScannedCode = null;
});
},
),
);
}
}
Item-BottomSheet (Menge erfassen):
class _ItemBottomSheet extends ConsumerStatefulWidget {
final String barcode;
final VoidCallback onDone;
const _ItemBottomSheet({
required this.barcode,
required this.onDone,
});
@override
ConsumerState<_ItemBottomSheet> createState() => _ItemBottomSheetState();
}
class _ItemBottomSheetState extends ConsumerState<_ItemBottomSheet> {
late TextEditingController _quantityController;
late TextEditingController _noteController;
late TextEditingController _nameController;
Item? _currentItem;
bool _isLoading = true;
@override
void initState() {
super.initState();
_quantityController = TextEditingController(text: '1');
_noteController = TextEditingController();
_nameController = TextEditingController();
_loadItem();
}
Future<void> _loadItem() async {
final repository = ref.read(itemsRepositoryProvider);
final items = await repository.getAllItems(searchQuery: widget.barcode);
if (items.isNotEmpty && items.first.code == widget.barcode) {
setState(() {
_currentItem = items.first;
_quantityController.text = _currentItem!.quantity.toString();
_noteController.text = _currentItem!.note ?? '';
_nameController.text = _currentItem!.name;
_isLoading = false;
});
} else {
setState(() {
_nameController.text = 'Artikel ${widget.barcode}';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: CircularProgressIndicator()),
);
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 16,
right: 16,
top: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Barcode: ${widget.barcode}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
TextField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Artikelname'),
),
const SizedBox(height: 16),
TextField(
controller: _noteController,
decoration: const InputDecoration(labelText: 'Notiz (optional)'),
),
const SizedBox(height: 16),
Row(
children: [
IconButton(
onPressed: () {
final current = int.tryParse(_quantityController.text) ?? 0;
_quantityController.text = (current - 1).clamp(0, 999999).toString();
},
icon: const Icon(Icons.remove_circle_outline),
color: Colors.red,
iconSize: 32,
),
Expanded(
child: TextField(
controller: _quantityController,
decoration: const InputDecoration(labelText: 'Menge'),
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
),
),
IconButton(
onPressed: () {
final current = int.tryParse(_quantityController.text) ?? 0;
_quantityController.text = (current + 1).clamp(0, 999999).toString();
},
icon: const Icon(Icons.add_circle_outline),
color: Colors.green,
iconSize: 32,
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
widget.onDone();
Navigator.pop(context);
},
child: const Text('Abbrechen'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () async {
final repository = ref.read(itemsRepositoryProvider);
final quantity = int.tryParse(_quantityController.text) ?? 0;
await repository.createOrUpdateItem(
code: widget.barcode,
name: _nameController.text.isNotEmpty
? _nameController.text
: 'Artikel ${widget.barcode}',
note: _noteController.text.isNotEmpty
? _noteController.text
: null,
quantity: quantity,
);
ref.read(itemsListProvider.notifier).loadItems();
widget.onDone();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Artikel ${_currentItem == null ? "erfasst" : "aktualisiert"}: $quantity Stück',
),
backgroundColor: Colors.green,
),
);
},
child: const Text('Speichern'),
),
),
],
),
const SizedBox(height: 16),
],
),
);
}
@override
void dispose() {
_quantityController.dispose();
_noteController.dispose();
_nameController.dispose();
super.dispose();
}
}
CSV-Export:
import 'package:csv/csv.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
Future<void> exportToCSV(List<Item> items) async {
// CSV-Daten erstellen
List<List<dynamic>> rows = [];
// Header
rows.add(['Barcode', 'Artikelname', 'Menge', 'Notiz', 'Erfasst am']);
// Daten
for (var item in items) {
rows.add([
item.code,
item.name,
item.quantity,
item.note ?? '',
item.createdAt.toIso8601String(),
]);
}
// CSV-String generieren
String csv = const ListToCsvConverter().convert(rows);
// In Datei schreiben
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/inventur_${DateTime.now().millisecondsSinceEpoch}.csv';
final file = File(path);
await file.writeAsString(csv);
// Teilen
await Share.shareXFiles(
[XFile(path)],
subject: 'Inventur Export',
text: 'Inventur-Daten (${items.length} Artikel)',
);
}
4.3 EAN-13-Validierung (Fehlerquote senken)
Problem: Tippfehler aus Simulationsmodus → ungültige Codes in DB!
Lösung: Prüfziffer-Validierung für EAN-13 (reduziert Fehlerquote um >90%).
bool isValidEan13(String code) {
final c = code.replaceAll(RegExp(r'\D'), ''); // Nur Ziffern
if (c.length != 13) return false;
final sum = List.generate(12, (i) =>
int.parse(c[i]) * (i.isEven ? 1 : 3)
).reduce((a, b) => a + b);
final check = (10 - (sum % 10)) % 10;
return check == int.parse(c[12]);
}
// Verwendung im Simulationsmodus:
void _processScan(String barcode) {
if (!isValidEan13(barcode)) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text("Ungültiger Barcode"),
content: Text("EAN-13 Prüfziffer stimmt nicht. Bitte überprüfen Sie die Eingabe."),
),
);
return;
}
_saveToDatabase(barcode);
}
💡 Tipp: Validierung sofort im Simulationsmodus → User merkt Fehler früh.
4.4 Kosten & Timeline (Inventur-Scanner ähnlich)
Features:
- Barcode-Scanning (EAN-13, QR, Code 128)
- Offline-First (SQLite/Hive)
- Item-Management (CRUD)
- Quantity-Tracking (+/- Buttons)
- Notizen
- CSV-Export + Share
- Simulationsmodus (für Screencasts)
- Torch-Support
- Android + iOS (Flutter)
Kosten:
- App-Entwicklung (Flutter): 18-25k € (2-3 Monate)
- Testing (Unit/Integration): 3-5k € (1 Woche)
- Design/UX: 3-5k € (1 Woche)
- Deployment + CI/CD: 1-2k €
- Gesamt: 25-37k € (Netto)
Timeline: 2,5-3,5 Monate (Konzept → MVP → Testing → Launch)
Hardware: Keine spezielle Hardware nötig (Smartphone-Kamera reicht).
5. Implementation: mobile_scanner Package
5.1 Package-Setup
Dependency (pubspec.yaml):
dependencies:
mobile_scanner: ^5.2.3
permission_handler: ^11.3.1
Android-Konfiguration (bereits in 2.1 gezeigt)
iOS-Konfiguration (bereits in 2.2 gezeigt)
5.2 Basis-Scanner
import 'package:mobile_scanner/mobile_scanner.dart';
class SimpleScannerScreen extends StatefulWidget {
const SimpleScannerScreen({super.key});
@override
State<SimpleScannerScreen> createState() => _SimpleScannerScreenState();
}
class _SimpleScannerScreenState extends State<SimpleScannerScreen> {
MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates, // Nur 1x pro Code
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Barcode Scanner'),
actions: [
IconButton(
icon: const Icon(Icons.flip_camera_ios),
onPressed: () => controller.switchCamera(),
),
IconButton(
icon: const Icon(Icons.flashlight_on),
onPressed: () => controller.toggleTorch(),
),
],
),
body: MobileScanner(
controller: controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
print('Barcode gefunden: ${barcode.rawValue}');
print('Format: ${barcode.format.name}');
}
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
5.3 Continuous-Scan-Mode (Logistik)
Use-Case: Pakete schnell hintereinander scannen (Logistik, Wareneingang) → DetectionSpeed.normal statt noDuplicates.
MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal, // Erlaubt schnelles Mehrfach-Scannen
);
MobileScanner(
controller: controller,
onDetect: (capture) {
for (final barcode in capture.barcodes) {
if (barcode.rawValue != null) {
_addToQueue(barcode.rawValue!);
_playBeepSound(); // Akustisches Feedback nach jedem Scan
}
}
},
);
💡 UX-Tipp: Akustisches Feedback (Beep) nach jedem Scan → User weiß: Scan erfolgreich.
5.4 Unterstützte Formate
mobile_scanner unterstützt:
- 1D: Code 39, Code 93, Code 128, EAN-8, EAN-13, UPC-A, UPC-E, Codabar, ITF
- 2D: QR-Code, Data Matrix, Aztec, PDF417
Format-Check:
onDetect: (capture) {
for (final barcode in capture.barcodes) {
switch (barcode.format) {
case BarcodeFormat.ean13:
print('EAN-13: ${barcode.rawValue}');
break;
case BarcodeFormat.qrCode:
print('QR-Code: ${barcode.rawValue}');
break;
case BarcodeFormat.code128:
print('Code 128: ${barcode.rawValue}');
break;
default:
print('Anderes Format: ${barcode.format.name}');
}
}
},
5.5 Barcode-Generator (Optional)
Use-Case: User will eigene Barcodes erstellen (z.B. Asset-Labels, Custom-Inventur-Codes).
Packages:
barcode_widget(UI-Widget für Flutter)barcode(Generator-Library)
Code-Beispiel:
import 'package:barcode_widget/barcode_widget.dart';
// QR-Code generieren
BarcodeWidget(
barcode: Barcode.qrCode(),
data: 'https://app-entwicklerin.de',
width: 200,
height: 200,
);
// EAN-13 generieren
BarcodeWidget(
barcode: Barcode.ean13(),
data: '4012345678901',
width: 300,
height: 100,
);
// Als PNG exportieren + drucken
import 'package:barcode/barcode.dart';
import 'dart:ui' as ui;
Future<Uint8List> generateBarcodePNG(String data) async {
final generator = Barcode.code128();
final svg = generator.toSvg(data, width: 300, height: 100);
// SVG → PNG konvertieren (für Druck)
// ... (via flutter_svg oder image package)
return pngBytes;
}
💡 Einsatzgebiete:
- Asset-Labels drucken (QR-Codes für IT-Equipment)
- Custom-Inventur-Codes generieren
- Ticket-Codes erstellen
6. Use-Cases: Von Inventur bis Ticket-System
6.1 Inventur (Retail, Lager, Werkstatt)
Anforderungen:
- EAN-13 scannen (Produkte)
- Offline-Speicherung (Lager hat oft kein WLAN)
- Menge erfassen (+/- Buttons)
- CSV-Export für Buchhaltung
Besonderheiten:
- Torch wichtig (Lagerhallen dunkel)
- Duplicate-Prevention (gleicher Artikel mehrfach gescannt)
- Simulationsmodus (falls Barcode unleserlich → manuell eingeben)
6.2 Ticket-Scanning (Events, Konzerte, Messen)
Anforderungen:
- QR-Code scannen (Ticket-Codes)
- Offline-Validierung (Server-Ausfall = Event-Disaster!)
- Einmal-Scan (Ticket ungültig nach 1x Scannen)
- Status-Feedback (Grün = gültig, Rot = bereits gescannt, Gelb = ungültig)
Besonderheiten:
- Schnelligkeit wichtig (Warteschlangen vermeiden)
- Offline-Liste vorheriger Scans (SQLite)
- Sync mit Backend (nach Event: Alle Scans hochladen)
Code-Snippet (Ticket-Validierung):
Future<TicketStatus> validateTicket(String ticketCode) async {
// Lokale DB prüfen (bereits gescannt?)
final scanned = await db.isTicketScanned(ticketCode);
if (scanned) {
return TicketStatus.alreadyScanned;
}
// Ticket-Code-Format validieren
if (!_isValidTicketFormat(ticketCode)) {
return TicketStatus.invalid;
}
// Als gescannt markieren (SQLite)
await db.markTicketAsScanned(ticketCode, DateTime.now());
return TicketStatus.valid;
}
enum TicketStatus { valid, alreadyScanned, invalid }
6.3 Wareneingang/Logistik
Anforderungen:
- Code 128 scannen (Versandlabels, Paletten)
- Mehrfach-Scan (Scan, Menge eingeben, nächster Scan)
- Standort-Tracking (GPS: Wo wurde Palette abgeladen?)
- Foto-Dokumentation (Zustand der Ware)
6.4 Asset-Tracking (IT, Maschinen, Werkzeuge)
Anforderungen:
- Data Matrix oder QR (Klein-Teile)
- Lifecycle-Tracking (Gekauft, In Nutzung, Wartung, Ausgeschieden)
- Wartungsintervalle (Maschine fällig für Prüfung)
- Standort-Historie (Wo war Asset zuletzt?)
6.5 Zeiterfassung (Check-in/Check-out)
Anforderungen:
- QR-Code auf Badge (Mitarbeiter-Ausweis)
- Check-in/Check-out (Kommt um 8:00, geht um 17:00)
- Offline-First (Baustellen ohne Internet)
- GPS-Validierung (Ist Mitarbeiter wirklich vor Ort?)
7. Kosten & Timeline
7.1 Projekttypen
Typ A: Basic Scanner-App (nur Scan + Liste)
- Features: Barcode-Scan, Simple Liste, keine DB, kein Export
- Kosten: 12-18k €
- Timeline: 1,5-2 Monate
Typ B: Inventur-App (mit CRUD + Export)
- Features: Scan, SQLite, CRUD, CSV-Export, Simulationsmodus, Torch
- Kosten: 25-37k €
- Timeline: 2,5-3,5 Monate
Typ C: Enterprise mit Backend (Ticket-System, Asset-Tracking)
- Features: Scan, Backend-Sync, User-Management, Admin-Dashboard, Reporting
- Kosten: 40-55k €
- Timeline: 3-4 Monate
7.2 Kostenaufschlüsselung (Typ B – Inventur-App)
| Position | Aufwand | Kosten (Netto) |
|---|---|---|
| Anforderungsanalyse | 2-3 Tage | 1.500-2.250 € |
| UI/UX-Design | 4-6 Tage | 3.000-4.500 € |
| App-Entwicklung (Flutter) | 24-32 Tage | 18.000-24.000 € |
| SQLite/Hive-Integration | 3-5 Tage | 2.250-3.750 € |
| CSV-Export + Share | 2-3 Tage | 1.500-2.250 € |
| Testing (Unit/Integration) | 4-6 Tage | 3.000-4.500 € |
| Deployment + CI/CD | 2-3 Tage | 1.500-2.250 € |
| Gesamt | 41-58 Tage | 30.750-43.500 € |
Hinweis: Bandbreiten aus DACH-Projekten; abhängig von Seniorität, Projekt-Scope und Region. Tagessatz: 750 €.
7.3 Laufende Kosten (nach Launch)
- App-Store-Gebühren: 25 € (Google Play, einmalig), 99 € (Apple, jährlich)
- Wartung/Support: 400-800 €/Monat (Bug-Fixes, OS-Updates)
- Backend (falls Enterprise): 30-100 €/Monat (AWS/Hetzner)
8. Scanner-App vs. Web-App (PWA)
| Kriterium | Native Scanner-App | Web-App (PWA) | Empfehlung |
|---|---|---|---|
| Kamera-Zugriff | ✅ Direkt, schnell (AVFoundation/Camera2) | ⚠️ Langsamer (WebRTC, Browser-Overhead) | 🏆 Native (schneller) |
| Offline-Funktionalität | ✅ SQLite, volle Kontrolle | ⚠️ IndexedDB, limitiert | 🏆 Native (robuster) |
| Performance | ✅ 60 FPS, Auto-Focus | ⚠️ 20-30 FPS, Browser-Limits | 🏆 Native (flüssiger) |
| Installation | ⚠️ App Store Download | ✅ Kein Download (Browser) | 🏆 PWA (einfacher) |
| Kosten | ⚠️ 25-40k (Native App) | ✅ 8-15k (PWA) | 🏆 PWA (günstiger) |
| Torch (Taschenlampe) | ✅ Volle Kontrolle | ❌ Nicht verfügbar (Browser-Limit) | 🏆 Native |
| Auto-Focus/Exposure | ✅ Volle Kontrolle (Camera2/AVFoundation) | ⚠️ Browser-limitiert (keine manuelle Kontrolle) | 🏆 Native |
| Barcode-Formate | ✅ Alle (via mobile_scanner) | ⚠️ Limitiert (Browser-APIs) | 🏆 Native |
💡 Faustregel:
- Native App: Wenn Offline-Funktionalität wichtig, Torch/Focus nötig, schnelles Scannen kritisch (Inventur, Logistik, Tickets)
- PWA: Wenn Budget <15k, einmalige Nutzung (Restaurant-Menü, URL-Scan), keine Offline-Anforderung
⚠️ PWA-Limitierungen explizit:
- Kein Torch (Taschenlampe) verfügbar
- Kein manueller Focus/Exposure Lock
- Langsamer Kamera-Zugriff (WebRTC-Overhead)
9. Checkliste: Scanner-App-Projekt starten
Vor Projektstart
- Use-Case definieren: Inventur? Tickets? Logistik? Asset-Tracking?
- Barcode-Typen festlegen: EAN-13, QR, Code 128, Data Matrix?
- Offline-Anforderung: Muss App ohne Internet funktionieren?
- Export-Format: CSV, JSON, PDF, Excel?
- Budget kalkulieren: 12k (Basic) vs. 30k (Inventur) vs. 50k (Enterprise)?
- Datenschutz klären: Keine Bilddaten speichern – nur dekodierte Barcodes + Metadaten (Zeit, Menge). Bei User-Bezug: AVV + Löschkonzept (30/90 Tage)
Technische Entscheidungen
- Framework: Flutter (Cross-Platform) vs. Native (iOS/Android separat)?
- Scanner-Package: mobile_scanner (Flutter empfohlen)
- Lokale DB: SQLite (Drift), Hive, oder Isar?
- Export-Format: CSV (am häufigsten), JSON, PDF?
- Simulationsmodus: Ja (für Screencasts/Demo) oder Nein?
Nach Entwicklung
- Field-Test: Real-World-Testing (schlechtes Licht, schnelle Scans, Duplikate)
- Permission-Testing: iOS/Android Permission-Dialoge testen
- Offline-Testing: WLAN/Mobile-Daten deaktivieren → funktioniert App?
- Export-Testing: CSV öffnet in Excel? Format korrekt?
- Performance: Scan-Speed < 2 Sekunden pro Code (messbar)
10. Fazit: Scanner-Apps sind schnell, praktisch, und offline-fähig
Scanner-Apps sind perfekt für:
- ✅ Inventur (Retail, Lager, Werkstatt)
- ✅ Ticket-Scanning (Events, Konzerte, Messen)
- ✅ Logistik (Wareneingang, Versand, Paletten-Tracking)
- ✅ Asset-Tracking (IT-Equipment, Maschinen, Werkzeuge)
- ✅ Zeiterfassung (Check-in/Check-out auf Baustellen)
Aber:
- ⚠️ Camera-Permissions tricky (User kann ablehnen → Fallback nötig!)
- ⚠️ Performance kritisch (langsames Scannen nervt → Auto-Focus, Torch, Duplicate-Prevention)
- ⚠️ Offline-Funktionalität Pflicht für Business-Apps (SQLite, CSV-Export)
Alternativen prüfen:
- PWA: Wenn Budget <15k, einmalige Nutzung, keine Offline-Anforderung
- Native App: Wenn Torch, schnelles Scannen, Offline-Funktionalität wichtig
Kosten-Richtwerte (Netto):
- Basic Scanner: 12-18k €, 1,5-2 Monate
- Inventur-App (mit CRUD): 25-37k €, 2,5-3,5 Monate
- Enterprise (mit Backend): 40-55k €, 3-4 Monate
Hardware: Keine spezielle Hardware nötig (Smartphone-Kamera reicht).
Du willst eine Scanner-App entwickeln lassen?
Ich entwickle seit 25+ Jahren Business-Apps, davon 5+ Jahre mit Barcode/QR-Scanning (Inventur, Logistik, Ticket-Systeme). Offline-First, Performance und UX sind meine Standards.
Meine Scanner-Projekte:
- Inventur-Scanner App: EAN-13 + QR + Code 128, Offline (SQLite), CSV-Export, Simulationsmodus
- FM 24 (mit QR): Wächterkontroll-App mit QR-Checkpoint-Scanning + GPS
Was du bekommst:
- ✅ Realistische Einschätzung: Kosten, Timeline, Barcode-Typen
- ✅ Performance-Optimierung: Auto-Focus, Torch, Duplicate-Prevention, Scan-Speed < 2 Sek
- ✅ Offline-First: SQLite/Hive, CSV-Export, funktioniert ohne Internet
- ✅ Simulationsmodus: Für Screencasts/Demos ohne Kamera
Weitere hilfreiche Artikel:
- NFC-App-Entwicklung 2025 – NFC vs. QR: Wann welcher?
- Offline-App-Entwicklung 2025 – SQLite, Sync-Strategien
- App-Sicherheit & DSGVO 2025 – Security-by-Design
- Flutter App-Entwicklung 2025 – Der komplette Guide
- Business-App-Entwicklung 2025 – B2B-Apps für Unternehmen
- App-Entwicklung Kosten 2025 – Was kostet eine Scanner-App?
- React Native vs. Flutter 2025 – Welches Framework?
- MVP-Entwicklung 2025 – Schneller Launch
- Android App-Entwicklung 2025 – Camera2 API
- iOS App-Entwicklung 2025 – AVFoundation
Hinweis: Alle Preise netto.
Ihr App-Projekt besprechen?
Lassen Sie uns in einem kostenlosen Erstgespräch über Ihre Anforderungen sprechen.
Jetzt Kontakt aufnehmen