Flutter Mobile Scanner shows black screen after scanning barcode


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

  1. Is this a known issue with mobile_scanner?

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

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


🙏 Any help would be appreciated!

1
Apr 6 at 8:09 AM
User AvatarHiranye Vithange
#android#flutter#barcode-scanner

Accepted Answer

The problem with your code is that you are not receiving the value the right way or you are popping to nothing.

I've tested your code and applied some edits. I've added a home page with a button. When pressing the button it opens your barcode page. So, when popping from the barcode page, the app returns to the home page (it can find some page to return to!).

Here are the core edits:

1- Added a home page. I'll attach the code below.

2- Calling Navigator.push with a return value to a variable, so that I can view it in a Text() widget.

3- Home page should be of type StatefulWidget and you should use SetState() to update the page after popping.

Code for home_page.dart

import 'package:flutter/material.dart'; import 'package:just_test/scanner.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { String? returnValue; void buttonOnPressed() async { returnValue = await Navigator.push( context, MaterialPageRoute(builder: (context) => BarcodeScannerScreen()), ); setState(() {}); // to refresh the screen and display the value } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => buttonOnPressed(), child: Text('Go to Scanner Page'), ), SizedBox(height: 40,), Text('Value: $returnValue'), ] ), ), ); } }
User AvatarGamal Othman
Apr 6 at 10:55 AM
-1