Flutter Mobile Scanner shows black screen after scanning barcode

1 day ago 2
ARTICLE AD BOX

Flutter Mobile Scanner shows black screen after scanning barcode

I am using the mobile_scanner package in Flutter to scan barcodes. The scanner works correctly for detecting the barcode, but after scanning, the screen turns black and does not recover properly.

🔍 What I’m trying to do

Open a barcode scanner screen

Scan a barcode

Return the scanned value using Navigator.pop()

❗ Problem

After a successful scan:

The camera preview turns black

Sometimes the screen freezes

When navigating back to the scanner, the camera does not start properly


📦 Package

mobile_scanner: ^7.2.0

🧩 My implementation

Here is my scanner logic:

// lib/screens/business_pos/barcode_scanner_screen.dart import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:salesapp/constants/colors.dart'; class BarcodeScannerScreen extends StatefulWidget { const BarcodeScannerScreen({super.key}); @override State<BarcodeScannerScreen> createState() => _BarcodeScannerScreenState(); } class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> with SingleTickerProviderStateMixin { final MobileScannerController _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, torchEnabled: false, autoStart: true, ); bool _torchOn = false; bool _detected = false; bool _isProcessing = false; // HARD LOCK to prevent multiple detections // Scan line animation late AnimationController _scanLineController; late Animation<double> _scanLineAnim; @override void initState() { super.initState(); _scanLineController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1800), )..repeat(reverse: true); _scanLineAnim = CurvedAnimation( parent: _scanLineController, curve: Curves.easeInOut, ); } @override void dispose() { _controller.stop(); _controller.dispose(); _scanLineController.dispose(); super.dispose(); } void _onDetect(BarcodeCapture capture) async { if (_isProcessing || !mounted) return; final value = capture.barcodes.firstOrNull?.rawValue; if (value == null) return; _isProcessing = true; // 🔒 HARD LOCK try { await _controller.stop(); } catch (e) { debugPrint('Camera stop error: $e'); } if (!mounted) return; // small delay prevents navigator lock await Future.delayed(const Duration(milliseconds: 150)); if (!mounted) return; Navigator.of(context).pop(value); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // ── Camera feed ── MobileScanner( controller: _controller, onDetect: _onDetect, errorBuilder: (context, error, child) { return Container( color: Colors.black, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline_rounded, color: Colors.white, size: 48, ), const SizedBox(height: 16), Text( _cameraErrorMessage(error), style: GoogleFonts.manrope( color: Colors.white, fontSize: 14, ), textAlign: TextAlign.center, ), const SizedBox(height: 20), GestureDetector( onTap: () async { await _controller.stop(); await Future.delayed( const Duration(milliseconds: 300), ); await _controller.start(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), decoration: BoxDecoration( color: CBaseColor, borderRadius: BorderRadius.circular(12), ), child: Text( 'Retry', style: GoogleFonts.manrope( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), ), ], ), ), ); }, ), // ── Dark overlay with cutout ── _buildScanOverlay(), // ── Top bar ── Positioned( top: MediaQuery.of(context).padding.top + 12, left: 0, right: 0, child: _buildTopBar(), ), // ── Bottom controls ── Positioned( bottom: MediaQuery.of(context).padding.bottom + 24, left: 0, right: 0, child: _buildBottomControls(), ), // ── Scan line animation inside the cutout ── _buildScanLine(), ], ), ); } String _cameraErrorMessage(MobileScannerException error) { switch (error.errorCode) { case MobileScannerErrorCode.permissionDenied: return 'Camera permission denied.\nPlease enable it in Settings.'; case MobileScannerErrorCode.unsupported: return 'Camera not supported on this device.'; default: return 'Camera error. Please try again.'; } } // ── Overlay with transparent square cutout ── Widget _buildScanOverlay() { return LayoutBuilder( builder: (context, constraints) { final size = constraints.maxWidth * 0.65; final top = (constraints.maxHeight - size) / 2 - 40; final left = (constraints.maxWidth - size) / 2; return Stack( children: [ // Top dark Positioned( top: 0, left: 0, right: 0, child: Container(height: top, color: Colors.black54), ), // Bottom dark Positioned( top: top + size, left: 0, right: 0, bottom: 0, child: Container(color: Colors.black54), ), // Left dark Positioned( top: top, left: 0, width: left, height: size, child: Container(color: Colors.black54), ), // Right dark Positioned( top: top, right: 0, width: constraints.maxWidth - left - size, height: size, child: Container(color: Colors.black54), ), // Corner brackets Positioned( top: top, left: left, width: size, height: size, child: _buildCornerBrackets(size), ), ], ); }, ); } Widget _buildCornerBrackets(double size) { const thickness = 3.0; const length = 28.0; const radius = 8.0; return Stack( children: [ // Top-left Positioned( top: 0, left: 0, child: Container( width: length, height: thickness, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(radius), ), ), ), ), Positioned( top: 0, left: 0, child: Container( width: thickness, height: length, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(radius), ), ), ), ), // Top-right Positioned( top: 0, right: 0, child: Container( width: length, height: thickness, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( topRight: Radius.circular(radius), ), ), ), ), Positioned( top: 0, right: 0, child: Container( width: thickness, height: length, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( topRight: Radius.circular(radius), ), ), ), ), // Bottom-left Positioned( bottom: 0, left: 0, child: Container( width: length, height: thickness, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(radius), ), ), ), ), Positioned( bottom: 0, left: 0, child: Container( width: thickness, height: length, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(radius), ), ), ), ), // Bottom-right Positioned( bottom: 0, right: 0, child: Container( width: length, height: thickness, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( bottomRight: Radius.circular(radius), ), ), ), ), Positioned( bottom: 0, right: 0, child: Container( width: thickness, height: length, decoration: BoxDecoration( color: CBaseColor, borderRadius: const BorderRadius.only( bottomRight: Radius.circular(radius), ), ), ), ), ], ); } // ── Animated scan line ── Widget _buildScanLine() { return LayoutBuilder( builder: (context, constraints) { final size = constraints.maxWidth * 0.65; final top = (constraints.maxHeight - size) / 2 - 40; final left = (constraints.maxWidth - size) / 2; return Stack( // ✅ ADD THIS children: [ AnimatedBuilder( animation: _scanLineAnim, builder: (_, __) { final lineY = top + _scanLineAnim.value * size; return Positioned( top: lineY, left: left + 8, right: constraints.maxWidth - left - size + 8, child: Container( height: 2, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, CBaseColor.withOpacity(0.8), CBaseColor, CBaseColor.withOpacity(0.8), Colors.transparent, ], ), ), ), ); }, ), ], ); }, ); } Widget _buildTopBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ // Back GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.15)), ), child: const Icon( Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 18, ), ), ), const Spacer(), Column( children: [ Text( 'Scan Barcode', style: GoogleFonts.manrope( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, ), ), Text( 'Point camera at product barcode', style: GoogleFonts.manrope(fontSize: 11, color: Colors.white60), ), ], ), const Spacer(), // Torch GestureDetector( onTap: () { _controller.toggleTorch(); setState(() => _torchOn = !_torchOn); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 42, height: 42, decoration: BoxDecoration( color: _torchOn ? Colors.amber.withOpacity(0.3) : Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: _torchOn ? Colors.amber.withOpacity(0.6) : Colors.white.withOpacity(0.15), ), ), child: Icon( _torchOn ? Icons.flashlight_on_rounded : Icons.flashlight_off_rounded, color: _torchOn ? Colors.amber : Colors.white, size: 20, ), ), ), ], ), ); } Widget _buildBottomControls() { return Column( children: [ Text( 'Align barcode within the frame', style: GoogleFonts.manrope(fontSize: 13, color: Colors.white70), ), const SizedBox(height: 20), // Manual entry button GestureDetector( onTap: () async { final result = await showDialog<String>( context: context, builder: (_) => _ManualEntryDialog(), ); if (result != null && result.isNotEmpty && mounted) { Navigator.pop(context, result); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.keyboard_rounded, color: Colors.white, size: 18, ), const SizedBox(width: 8), Text( 'Enter manually', style: GoogleFonts.manrope( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ), ], ); } } // ── Manual barcode entry dialog ── class _ManualEntryDialog extends StatelessWidget { final _ctrl = TextEditingController(); @override Widget build(BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text( 'Enter Barcode', style: GoogleFonts.manrope( fontWeight: FontWeight.w700, color: const Color(0xFF1A2E1A), ), ), content: TextField( controller: _ctrl, autofocus: true, keyboardType: TextInputType.number, decoration: InputDecoration( hintText: 'e.g. 8901234560001', hintStyle: GoogleFonts.manrope(color: Colors.grey.shade400), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: CBaseColor), ), ), onSubmitted: (_) => Navigator.pop(context, _ctrl.text.trim()), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Cancel', style: GoogleFonts.manrope(color: Colors.grey)), ), ElevatedButton( onPressed: () => Navigator.pop(context, _ctrl.text.trim()), style: ElevatedButton.styleFrom( backgroundColor: CBaseColor, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: Text('Add', style: GoogleFonts.manrope(color: Colors.white)), ), ], ); } }

🧪 What I already tried

Calling _controller.stop() before navigating

Adding delay before restarting camera

Using autoStart: true and also manually calling start()

Handling errors using errorBuilder

Restarting scanner with a retry button

None of these fixed the black screen issue.


📱 Expected behavior

Camera should stop cleanly after scan

Screen should pop without any black flicker

When reopening scanner, camera should work normally


⚠️ Actual behavior

Black screen after scan

Camera preview sometimes does not restart

UI becomes unresponsive occasionally


❓ Questions

Is this a known issue with mobile_scanner?

What is the correct way to handle camera lifecycle when navigating?

Should I avoid calling start() manually when autoStart is enabled?


🙏 Any help would be appreciated!

Read Entire Article