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.
This commit is contained in:
your-name 2025-07-30 19:01:14 +03:00
parent a7136dd22b
commit 68a2803b6e
10 changed files with 97 additions and 558 deletions

View File

@ -7,7 +7,6 @@ import 'package:e_scan/provider_container.dart';
import 'package:e_scan/services/dio_service.dart';
import 'package:e_scan/services/token_provider.dart';
import 'package:e_scan/utils/utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:multiple_result/multiple_result.dart';
@ -16,31 +15,6 @@ class ApiCalls {
tokenProvider: providerContainer.read(tokenProvider),
);
static Future<Map<String, dynamic>?> fetchProduct(String barcode) async {
final Dio dio = Dio(
BaseOptions(baseUrl: 'https://world.openfoodfacts.org'),
);
try {
final response = await dio.get('/api/v0/product/$barcode.json');
if (response.statusCode == 200) {
final data = response.data;
if (data['status'] == 1) {
return data['product'];
} else {
debugPrint('Produit non trouvé');
return null;
}
} else {
debugPrint('Erreur réseau: ${response.statusCode}');
return null;
}
} catch (e) {
debugPrint('Erreur lors de la requête: $e');
return null;
}
}
static Future<Result<AuthModel, Error>> signIn({
required String email,
required String password,

View File

@ -29,41 +29,40 @@ class StockPickingRecordEntity {
@Id(assignable: true)
int id;
final String? priority;
final String? name;
final String? pickingTypeCode;
final String? state;
final String? origin;
final bool? useCreateLots;
final bool? isLocked;
final dynamic scheduledDate;
final dynamic dateDeadline;
final dynamic dateDone;
String? priority;
String? name;
String? pickingTypeCode;
String? state;
String? origin;
bool? useCreateLots;
bool? isLocked;
dynamic scheduledDate;
dynamic dateDeadline;
dynamic dateDone;
final dynamic productsAvailability;
final dynamic productsAvailabilityState;
dynamic productsAvailability;
dynamic productsAvailabilityState;
final dynamic jsonPopover;
final dynamic activityExceptionIcon;
final dynamic activityExceptionDecoration;
final dynamic showCheckAvailability;
final dynamic hasScrapMove;
final dynamic hasPackages;
final dynamic showNextPickings;
final dynamic pickingTypeEntirePacks;
dynamic jsonPopover;
dynamic activityExceptionIcon;
dynamic activityExceptionDecoration;
dynamic showCheckAvailability;
dynamic hasScrapMove;
dynamic hasPackages;
dynamic showNextPickings;
dynamic pickingTypeEntirePacks;
final ToOne<StockPickingCompanyEntity> companyId = ToOne();
final ToOne<StockPickingPartnerEntity> partnerId = ToOne();
final ToOne<StockPickingLocationEntity> locationId = ToOne();
final ToOne<StockPickingLocationEntity> locationDestId = ToOne();
final ToOne<StockPickingTypeEntity> pickingTypeId = ToOne();
ToOne<StockPickingCompanyEntity> companyId = ToOne();
ToOne<StockPickingPartnerEntity> partnerId = ToOne();
ToOne<StockPickingLocationEntity> locationId = ToOne();
ToOne<StockPickingLocationEntity> locationDestId = ToOne();
ToOne<StockPickingTypeEntity> pickingTypeId = ToOne();
@Backlink('pickingRecord')
final ToMany<MoveLineWithoutPackageEntity> moveLineIdsWithoutPackage =
ToMany();
ToMany<MoveLineWithoutPackageEntity> moveLineIdsWithoutPackage = ToMany();
@Backlink('pickingRecord')
final ToMany<MoveWithoutPackageEntity> moveIdsWithoutPackage = ToMany();
ToMany<MoveWithoutPackageEntity> moveIdsWithoutPackage = ToMany();
}
@Entity()
@ -78,7 +77,7 @@ class StockPickingPartnerEntity {
StockPickingPartnerEntity({this.id = 0, this.displayName});
@Id(assignable: true)
int id;
final String? displayName;
String? displayName;
}
@Entity()
@ -86,7 +85,7 @@ class StockPickingLocationEntity {
StockPickingLocationEntity({this.id = 0, this.completeName});
@Id(assignable: true)
int id;
final String? completeName;
String? completeName;
}
@Entity()
@ -94,7 +93,7 @@ class StockPickingTypeEntity {
StockPickingTypeEntity({this.id = 0, this.displayName});
@Id(assignable: true)
int id;
final String? displayName;
String? displayName;
}
@Entity()
@ -103,10 +102,10 @@ class MoveLineWithoutPackageEntity {
@Id(assignable: true)
int id;
final double? quantity;
double? quantity;
final ToOne<ProductEntity> productId = ToOne();
final ToOne<StockPickingRecordEntity> pickingRecord = ToOne();
ToOne<ProductEntity> productId = ToOne();
ToOne<StockPickingRecordEntity> pickingRecord = ToOne();
}
@Entity()
@ -115,11 +114,11 @@ class MoveWithoutPackageEntity {
@Id(assignable: true)
int id;
final double? quantity;
final double? productUomQty;
double? quantity;
double? productUomQty;
final ToOne<ProductEntity> productId = ToOne();
final ToOne<StockPickingRecordEntity> pickingRecord = ToOne();
ToOne<ProductEntity> productId = ToOne();
ToOne<StockPickingRecordEntity> pickingRecord = ToOne();
}
extension StockPickingRecordEntityExt on StockPickingRecordEntity {

View File

@ -1,4 +1,4 @@
import 'package:e_scan/backend/schema/product/product_model.dart';
import 'package:e_scan/backend/objectbox/entities/product/product_entity.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:flutter/material.dart';
@ -9,7 +9,7 @@ class ProductScannedComponent extends StatefulWidget {
this.onRescan,
this.onDetails,
});
final ProductModel productStruct;
final ProductEntity productStruct;
final Future Function()? onRescan;
final Function()? onDetails;

View File

@ -1,13 +1,7 @@
import 'dart:async';
import 'package:e_scan/backend/api/api_calls.dart';
import 'package:e_scan/backend/objectbox/entities/product/product_entity.dart';
import 'package:e_scan/backend/objectbox/objectbox_manager.dart';
import 'package:e_scan/backend/schema/product/product_model.dart';
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/router/go_secure_router_builder.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -102,6 +96,7 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
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");
@ -110,21 +105,14 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(code.displayValue!);
if (product != null) {
// find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue);
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity());
//show dialog
await showDialog(
barrierDismissible: false,
@ -146,18 +134,18 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: productStruct,
productStruct: product!,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
@ -177,25 +165,18 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
Future<void> _fakeBarcode() async {
final qrcodeValue = "0737628064502";
qrcodeFound = true;
final model = ref.read(receptionScanPageModelProvider.notifier);
mobileScannerController.stop();
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(qrcodeValue);
if (product != null) {
// find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue);
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity());
//show dialog
await showDialog(
barrierDismissible: false,
@ -217,18 +198,18 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: productStruct,
productStruct: product!,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
Navigator.of(context).pop();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),

View File

@ -1,3 +1,4 @@
import 'package:e_scan/backend/objectbox/entities/product/product_entity.dart';
import 'package:e_scan/backend/objectbox/entities/stock_picking/stock_picking_record_entity.dart';
import 'package:e_scan/backend/objectbox/objectbox_manager.dart';
import 'package:flutter/foundation.dart';
@ -31,6 +32,44 @@ class ReceptionScanPageModel
}
}
bool isProductExist({required String barcode}) {
final productBox = objectboxManager.store.box<ProductEntity>();
return productBox
.query(ProductEntity_.barcode.equals(barcode))
.build()
.findFirst() !=
null;
}
ProductEntity? getProduct({required String barcode}) {
final productBox = objectboxManager.store.box<ProductEntity>();
return productBox
.query(ProductEntity_.barcode.equals(barcode))
.build()
.findFirst();
}
void incrementMoveLineQuantity({required String barcode}) {
final moveLineBox = objectboxManager.store
.box<MoveLineWithoutPackageEntity>();
final productBox = objectboxManager.store.box<ProductEntity>();
final productEntity = productBox
.query(ProductEntity_.barcode.equals(barcode))
.build()
.findFirst();
final productId = productEntity?.id;
if (productId != null) {
final moveLineEntity = moveLineBox
.query(MoveLineWithoutPackageEntity_.productId.equals(productId))
.build()
.findFirst();
if (moveLineEntity != null) {
moveLineEntity.quantity = (moveLineEntity.quantity ?? 0) + 1;
moveLineBox.put(moveLineEntity);
}
}
}
Future handleBarcode({required BarcodeCapture barcodeCapture}) async {
try {
final stockPickingRecords = objectboxManager.store

View File

@ -1,6 +1,5 @@
export 'login/login_page.dart';
export 'product/product_form/product_form_page.dart';
export 'scanner/scanner_page.dart';
export 'splash/splash_page.dart';
export 'product/product_list/product_list_page.dart';
export 'profile/profile_page.dart';

View File

@ -1,424 +0,0 @@
import 'dart:async';
import 'package:e_scan/backend/api/api_calls.dart';
import 'package:e_scan/backend/objectbox/entities/product/product_entity.dart';
import 'package:e_scan/backend/objectbox/objectbox_manager.dart';
import 'package:e_scan/backend/schema/product/product_model.dart';
import 'package:e_scan/components/loading_progress_component.dart';
import 'package:e_scan/components/product_scanned_component.dart';
import 'package:e_scan/router/go_secure_router_builder.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannerPage extends ConsumerStatefulWidget {
const ScannerPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ScannerPageState();
}
class _ScannerPageState extends ConsumerState<ScannerPage>
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);
// 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}");
if (qrcodeValue == null) {
debugPrint("Qrcode non valide");
return;
}
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(code.displayValue!);
if (product != null) {
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity());
//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: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
}
Future<void> _fakeBarcode() async {
final qrcodeValue = "0737628064502";
qrcodeFound = true;
mobileScannerController.stop();
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(qrcodeValue);
if (product != null) {
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity());
//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: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
@override
Widget build(BuildContext context) {
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('Scanner', 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
? 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();
},
),
],
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -13,7 +13,6 @@ final appSecureRoutes = $appRoutes;
@TypedGoRoute<SecureRoute>(
path: '/SecurePage',
routes: [
TypedGoRoute<ScannerRoute>(path: 'ScannerPage'),
TypedGoRoute<ProductFormRoute>(path: 'ProductFormPage'),
TypedGoRoute<ProductListRoute>(path: 'ProductListPage'),
TypedGoRoute<ProfileRoute>(path: 'ProfilePage'),
@ -41,13 +40,6 @@ class SecureRoute extends GoRouteData with _$SecureRoute {
}
}
class ScannerRoute extends GoRouteData with _$ScannerRoute {
const ScannerRoute();
@override
Widget build(BuildContext context, GoRouterState state) => ScannerPage();
}
class ProductFormRoute extends GoRouteData with _$ProductFormRoute {
const ProductFormRoute({required this.id});
final int id;

View File

@ -13,7 +13,6 @@ RouteBase get $secureRoute => GoRouteData.$route(
factory: _$SecureRoute._fromState,
routes: [
GoRouteData.$route(path: 'ScannerPage', factory: _$ScannerRoute._fromState),
GoRouteData.$route(
path: 'ProductFormPage',
@ -73,26 +72,6 @@ mixin _$SecureRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location);
}
mixin _$ScannerRoute on GoRouteData {
static ScannerRoute _fromState(GoRouterState state) => const ScannerRoute();
@override
String get location => GoRouteData.$location('/SecurePage/ScannerPage');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin _$ProductFormRoute on GoRouteData {
static ProductFormRoute _fromState(GoRouterState state) =>
ProductFormRoute(id: int.parse(state.uri.queryParameters['id']!)!);

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.0.1+2
version: 0.1.0+3
environment:
sdk: ^3.8.1