Technologie & Architektur 22. September 2025 16 min Lesezeit

Barcode & QR-Scanner Apps 2025: Von Inventur bis Ticket-System

Barcode-Scanner-App entwickeln: Camera-Permissions (iOS/Android), Performance-Tricks, Offline-Scan mit SQLite. Real Case: Inventur-Scanner. Kosten 12-50k.

Carola Schulte
Carola Schulte
Zurück zum Blog

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

AspektZusammenfassung
Barcode-TypenEAN-13 (Retail), QR (URLs/VCards), Code 128 (Logistik), Data Matrix (Industrie)
Camera-PermissionsAndroid: Manifest + Runtime. iOS: Info.plist + NSCameraUsageDescription. Fallback: Manuelle Eingabe/CSV-Import
PerformanceAuto-Focus, Torch für schlechtes Licht, Duplicate-Prevention (lastScannedCode), Debouncing
Offline-ScanSQLite für lokale Speicherung, Sync bei Netzverbindung, CSV-Export
Kosten12-18k (Basic Scanner), 25-35k (Inventur mit CRUD), 40-50k (Enterprise mit Backend)
Timeline1,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

TypDimensionDatenkapazitätTypische Nutzung
EAN-131D13 ZiffernRetail (Produkte im Supermarkt)
EAN-81D8 ZiffernKleine Produkte (Zigaretten, Süßigkeiten)
Code 1281DVariableLogistik, Versand (Post, DHL)
Code 391DVariableIndustrie, Lagerverwaltung (älterer Standard)
QR-Code2DBis 4.296 ZeichenURLs, VCards, Tickets, Menüs
Data Matrix2DBis 3.116 ZeichenIndustrie (Klein-Teile, Elektronik, Pharma)
PDF4172DBis 1.800 ZeichenReisepässe, Führerscheine, Boarding-Pässe
Aztec Code2DBis 3.067 ZeichenTransport-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:

  1. Auto-Focus aktivieren (mobile_scanner macht das automatisch)
  2. Frame-Rate limitieren (30 FPS reichen, spart CPU)
  3. Scan-Bereich einschränken (Region of Interest / ROI → nur Mitte analysieren)
  4. Exposure Lock nach erstem Fokus (verhindert Über-/Unterbelichtung)
  5. 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:

  1. Torch aktivieren (bessere Beleuchtung = höhere Erfolgsrate)
  2. Mehrfach-Scans (wenn 1× fehlschlägt → automatisch nochmal probieren)
  3. Abstand variieren (näher/weiter weg = andere Fokus-Distanz)
  4. 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 &lt; 8) {
    _scanAttempts++;

    if (_scanAttempts &gt;= _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)

PositionAufwandKosten (Netto)
Anforderungsanalyse2-3 Tage1.500-2.250 €
UI/UX-Design4-6 Tage3.000-4.500 €
App-Entwicklung (Flutter)24-32 Tage18.000-24.000 €
SQLite/Hive-Integration3-5 Tage2.250-3.750 €
CSV-Export + Share2-3 Tage1.500-2.250 €
Testing (Unit/Integration)4-6 Tage3.000-4.500 €
Deployment + CI/CD2-3 Tage1.500-2.250 €
Gesamt41-58 Tage30.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)

KriteriumNative Scanner-AppWeb-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:

  1. NFC-App-Entwicklung 2025 – NFC vs. QR: Wann welcher?
  2. Offline-App-Entwicklung 2025 – SQLite, Sync-Strategien
  3. App-Sicherheit & DSGVO 2025 – Security-by-Design
  4. Flutter App-Entwicklung 2025 – Der komplette Guide
  5. Business-App-Entwicklung 2025 – B2B-Apps für Unternehmen
  6. App-Entwicklung Kosten 2025 – Was kostet eine Scanner-App?
  7. React Native vs. Flutter 2025 – Welches Framework?
  8. MVP-Entwicklung 2025 – Schneller Launch
  9. Android App-Entwicklung 2025 – Camera2 API
  10. 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