barcode_scanner/lib/pages/operation/reception/reception_scan_page.dart
your-name 68a2803b6e enhance: Enhances product scanning with local ObjectBox integration
Moves product lookup from external API calls to a local ObjectBox database. This improves performance and enables offline product identification during scanning.

Removes the standalone scanner page, consolidating barcode scanning functionality directly into the reception flow for a more streamlined user experience.

Updates ObjectBox entity fields by removing `final` modifiers, allowing the database to manage and update persisted data effectively. Introduces new methods in the reception scan model to support local product checks, retrieval, and quantity increment for scanned items.
2025-07-30 19:01:14 +03:00

421 lines
16 KiB
Dart

import 'dart:async';
import 'package:e_scan/components/loading_progress_component.dart';
import 'package:e_scan/components/product_scanned_component.dart';
import 'package:e_scan/pages/operation/reception/reception_scan_page_model.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ReceptionScanPage extends ConsumerStatefulWidget {
const ReceptionScanPage({super.key, required this.receptionId});
final int receptionId;
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_ReceptionScanPageState();
}
class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
with WidgetsBindingObserver {
final MobileScannerController mobileScannerController =
MobileScannerController(
// required options for the scanner
);
StreamSubscription<Object?>? _subscription;
bool qrcodeFound = false;
bool loading = false;
@override
void initState() {
super.initState();
// Start listening to lifecycle changes.
WidgetsBinding.instance.addObserver(this);
SchedulerBinding.instance.addPostFrameCallback((_) {
ref
.read(receptionScanPageModelProvider.notifier)
.getReceptionById(id: widget.receptionId);
});
// Start listening to the barcode events.
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
// Finally, start the scanner itself.
unawaited(mobileScannerController.start());
}
@override
void dispose() {
// Stop listening to lifecycle changes.
WidgetsBinding.instance.removeObserver(this);
// Stop listening to the barcode events.
unawaited(_subscription?.cancel());
_subscription = null;
// Dispose the widget itself.
// Finally, dispose of the controller.
mobileScannerController.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// If the controller is not ready, do not try to start or stop it.
// Permission dialogs can trigger lifecycle changes before the controller is ready.
if (!mobileScannerController.value.hasCameraPermission) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
// Restart the scanner when the app is resumed.
// Don't forget to resume listening to the barcode events.
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
unawaited(mobileScannerController.start());
case AppLifecycleState.inactive:
// Stop the scanner when the app is paused.
// Also stop the barcode events subscription.
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
}
}
Future<void> _handleBarcode(BarcodeCapture barcodeCapture) async {
if (qrcodeFound) return;
if (barcodeCapture.barcodes.isNotEmpty) {
qrcodeFound = true;
mobileScannerController.stop();
final code = barcodeCapture.barcodes.first;
final qrcodeValue = code.displayValue;
debugPrint("Code détecté : ${code.displayValue}");
final model = ref.read(receptionScanPageModelProvider.notifier);
if (qrcodeValue == null) {
debugPrint("Qrcode non valide");
return;
}
setState(() {
loading = true;
});
// find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue);
setState(() {
loading = false;
});
//show dialog
await showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) {
return Dialog(
elevation: 0,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
alignment: AlignmentDirectional(
0.0,
0.0,
).resolve(Directionality.of(context)),
child: GestureDetector(
onTap: () {
FocusScope.of(dialogContext).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
},
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: product!,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
}
Future<void> _fakeBarcode() async {
final qrcodeValue = "0737628064502";
qrcodeFound = true;
final model = ref.read(receptionScanPageModelProvider.notifier);
mobileScannerController.stop();
setState(() {
loading = true;
});
// find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue);
setState(() {
loading = false;
});
//show dialog
await showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) {
return Dialog(
elevation: 0,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
alignment: AlignmentDirectional(
0.0,
0.0,
).resolve(Directionality.of(context)),
child: GestureDetector(
onTap: () {
FocusScope.of(dialogContext).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
},
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: product!,
onRescan: () async {
qrcodeFound = false;
mobileScannerController.start();
Navigator.of(context).pop();
},
onDetails: () {
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(receptionScanPageModelProvider);
final reception = state.reception;
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
appBar: AppBar(
backgroundColor: AppTheme.of(context).primaryBackground,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: AppTheme.of(context).primaryText,
size: 24.0,
),
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
'Réception ${reception?.name}',
style: AppTheme.of(context).titleLarge,
),
actions: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(8.0, 0.0, 8.0, 0.0),
child: IconButton(
icon: Icon(Icons.help_outline, color: Colors.white, size: 24.0),
onPressed: () {
debugPrint('IconButton pressed ...');
},
),
),
],
centerTitle: true,
elevation: 0.0,
),
body: loading || state.loading
? Center(child: LoadingProgressComponent())
: SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
SizedBox(
width: double.infinity,
height: double.infinity,
child: MobileScanner(
controller: mobileScannerController,
onDetectError: (error, stackTrace) {
debugPrint("===========> $error");
},
errorBuilder: (c, p2) {
return Center(child: Icon(Icons.error));
},
),
),
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.of(context).primaryBackground,
Colors.transparent,
AppTheme.of(context).primaryBackground,
],
stops: [0.0, 0.5, 1.0],
begin: AlignmentDirectional(0.0, -1.0),
end: AlignmentDirectional(0, 1.0),
),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
24.0,
40.0,
24.0,
0.0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Scanner le code',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).primaryText,
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0.0,
8.0,
0.0,
0.0,
),
child: Text(
'Positionnez le code-barres ou QR code dans le cadre',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).primaryText,
),
),
),
],
),
),
Align(
alignment: AlignmentDirectional(0.0, 0.0),
child: Container(
width: 280.0,
height: 280.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: Colors.white,
width: 3.0,
),
),
child: GestureDetector(
onTap: () {
_fakeBarcode();
qrcodeFound = false;
mobileScannerController.start();
},
child: Container(
width: 260.0,
height: 260.0,
decoration: BoxDecoration(
color: Color(0x33FFFFFF),
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code_scanner,
color: Colors.white.withValues(
alpha: .5,
),
size: 64.0,
),
],
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(40),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
color: AppTheme.of(context).primaryText,
iconSize: 2,
icon: Icon(
mobileScannerController.torchEnabled
? Icons.flash_off
: Icons.flash_on,
color: AppTheme.of(context).primaryText,
size: 28.0,
),
onPressed: () {
mobileScannerController.toggleTorch();
},
),
],
),
),
],
),
),
),
],
),
),
);
}
}