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.
Open a barcode scanner screen
Scan a barcode
Return the scanned value using Navigator.pop()
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
mobile_scanner: ^7.2.0
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)),
),
],
);
}
}
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.
Camera should stop cleanly after scan
Screen should pop without any black flicker
When reopening scanner, camera should work normally
Black screen after scan
Camera preview sometimes does not restart
UI becomes unresponsive occasionally
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?
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}); 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 } 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'), ] ), ), ); } }