feat: Implement local product storage using ObjectBox
Configures the ObjectBox ProductEntity to allow assigning IDs. Adds a helper to convert ProductStruct to ProductEntity for easier storage. Saves scanned products directly to the local ObjectBox store. Updates the ProductListPage to fetch and display locally stored products from ObjectBox upon initialization. Includes minor UI adjustments in the scanned product component and scanner page, and fixes loading state handling in HomePageModel.
This commit is contained in:
parent
44289e29dc
commit
09405b5da7
@ -4,7 +4,7 @@ import 'package:objectbox/objectbox.dart';
|
||||
/// Modèle de base de données ObjectBox
|
||||
@Entity()
|
||||
class ProductEntity {
|
||||
@Id()
|
||||
@Id(assignable: true)
|
||||
int id;
|
||||
|
||||
String? code;
|
||||
|
@ -12,7 +12,7 @@
|
||||
"id": "1:1853465479129290672",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
"flags": 129
|
||||
},
|
||||
{
|
||||
"id": "2:4521897043130066476",
|
||||
|
@ -29,7 +29,7 @@ final _entities = <obx_int.ModelEntity>[
|
||||
id: const obx_int.IdUid(1, 1853465479129290672),
|
||||
name: 'id',
|
||||
type: 6,
|
||||
flags: 1,
|
||||
flags: 129,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(2, 4521897043130066476),
|
||||
|
@ -3,6 +3,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:barcode_scanner/backend/objectbox/objectbox.g.dart';
|
||||
export 'package:barcode_scanner/backend/objectbox/objectbox.g.dart';
|
||||
|
||||
late ObjectboxManager objectboxManager;
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'product_struct.freezed.dart';
|
||||
@ -18,3 +19,14 @@ abstract class ProductStruct with _$ProductStruct {
|
||||
factory ProductStruct.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProductStructFromJson(json);
|
||||
}
|
||||
|
||||
extension ProductStructExt on ProductStruct {
|
||||
ProductEntity get toEntity => ProductEntity(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
image: image,
|
||||
);
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class _ProductScannedComponentState extends State<ProductScannedComponent> {
|
||||
alignment: Alignment.topCenter,
|
||||
child: Image.network(
|
||||
widget.productStruct.image ?? '',
|
||||
height: MediaQuery.sizeOf(context).height * .2,
|
||||
height: MediaQuery.sizeOf(context).height * .12,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
@ -164,7 +164,9 @@ class _ProductScannedComponentState extends State<ProductScannedComponent> {
|
||||
},
|
||||
child: Text(
|
||||
'Voir détails',
|
||||
style: AppTheme.of(context).bodyMedium,
|
||||
style: AppTheme.of(context).bodyMedium.override(
|
||||
color: AppTheme.of(context).white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -29,7 +29,7 @@ class HomePageModel extends StateNotifier<HomePageState> {
|
||||
Future getUserConnected() async {
|
||||
state = state.copyWith(loading: true);
|
||||
final user = await UserStruct(id: '1').getFromLocalStorage();
|
||||
state = state.copyWith(user: user);
|
||||
state = state.copyWith(user: user, loading: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
import 'package:barcode_scanner/components/loading_progress_component.dart';
|
||||
import 'package:barcode_scanner/pages/product_list_page/product_list_page_model.dart';
|
||||
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
|
||||
import 'package:barcode_scanner/themes/app_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ProductListPage extends ConsumerStatefulWidget {
|
||||
@ -11,8 +15,17 @@ class ProductListPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ProductListPageState extends ConsumerState<ProductListPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(productListPageModelProvider.notifier).getProductsLocal();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(productListPageModelProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
appBar: AppBar(
|
||||
@ -33,6 +46,55 @@ class _ProductListPageState extends ConsumerState<ProductListPage> {
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
elevation: 0,
|
||||
),
|
||||
body: state.loading
|
||||
? Center(child: LoadingProgressComponent())
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
if (state.products.isEmpty) {
|
||||
return Center(child: Text('Aucun donnée trouvée'));
|
||||
} else {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
itemCount: state.products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = state.products[index];
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
ProductFormRoute().push(context);
|
||||
},
|
||||
leading: Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(product.image ?? ''),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
product.name ?? '',
|
||||
style: AppTheme.of(context).titleMedium,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Code: ${product.id}",
|
||||
style: AppTheme.of(context).bodyMedium,
|
||||
),
|
||||
Text(
|
||||
"Quatité: ${product.quantity ?? ''}",
|
||||
style: AppTheme.of(context).bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
35
lib/pages/product_list_page/product_list_page_model.dart
Normal file
35
lib/pages/product_list_page/product_list_page_model.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart';
|
||||
import 'package:barcode_scanner/backend/objectbox/objectbox_manager.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'product_list_page_model.freezed.dart';
|
||||
|
||||
/// The provider for the AuthViewModel, using Riverpod's StateNotifierProvider
|
||||
/// with autoDispose to manage the lifecycle of the view model.
|
||||
final productListPageModelProvider =
|
||||
StateNotifierProvider<ProductListPageModel, ProductListPageState>((ref) {
|
||||
return ProductListPageModel();
|
||||
});
|
||||
|
||||
class ProductListPageModel extends StateNotifier<ProductListPageState> {
|
||||
/// Constructor initializes the TaskRepository using the provider reference.
|
||||
ProductListPageModel() : super(const ProductListPageState());
|
||||
|
||||
Future getProductsLocal() async {
|
||||
Box<ProductEntity> productStore = objectboxManager.store
|
||||
.box<ProductEntity>();
|
||||
state = state.copyWith(loading: true);
|
||||
final products = await productStore.getAllAsync();
|
||||
state = state.copyWith(products: products, loading: false);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ProductListPageState with _$ProductListPageState {
|
||||
const factory ProductListPageState({
|
||||
@Default(<ProductEntity>[]) List<ProductEntity> products,
|
||||
@Default(false) bool loading,
|
||||
}) = _ProductListPageState;
|
||||
}
|
163
lib/pages/product_list_page/product_list_page_model.freezed.dart
Normal file
163
lib/pages/product_list_page/product_list_page_model.freezed.dart
Normal file
@ -0,0 +1,163 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'product_list_page_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ProductListPageState implements DiagnosticableTreeMixin {
|
||||
|
||||
List<ProductEntity> get products; bool get loading;
|
||||
/// Create a copy of ProductListPageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ProductListPageStateCopyWith<ProductListPageState> get copyWith => _$ProductListPageStateCopyWithImpl<ProductListPageState>(this as ProductListPageState, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'ProductListPageState'))
|
||||
..add(DiagnosticsProperty('products', products))..add(DiagnosticsProperty('loading', loading));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductListPageState&&const DeepCollectionEquality().equals(other.products, products)&&(identical(other.loading, loading) || other.loading == loading));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(products),loading);
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'ProductListPageState(products: $products, loading: $loading)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ProductListPageStateCopyWith<$Res> {
|
||||
factory $ProductListPageStateCopyWith(ProductListPageState value, $Res Function(ProductListPageState) _then) = _$ProductListPageStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ProductEntity> products, bool loading
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ProductListPageStateCopyWithImpl<$Res>
|
||||
implements $ProductListPageStateCopyWith<$Res> {
|
||||
_$ProductListPageStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ProductListPageState _self;
|
||||
final $Res Function(ProductListPageState) _then;
|
||||
|
||||
/// Create a copy of ProductListPageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? products = null,Object? loading = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
products: null == products ? _self.products : products // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductEntity>,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ProductListPageState with DiagnosticableTreeMixin implements ProductListPageState {
|
||||
const _ProductListPageState({final List<ProductEntity> products = const <ProductEntity>[], this.loading = false}): _products = products;
|
||||
|
||||
|
||||
final List<ProductEntity> _products;
|
||||
@override@JsonKey() List<ProductEntity> get products {
|
||||
if (_products is EqualUnmodifiableListView) return _products;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_products);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool loading;
|
||||
|
||||
/// Create a copy of ProductListPageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ProductListPageStateCopyWith<_ProductListPageState> get copyWith => __$ProductListPageStateCopyWithImpl<_ProductListPageState>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'ProductListPageState'))
|
||||
..add(DiagnosticsProperty('products', products))..add(DiagnosticsProperty('loading', loading));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductListPageState&&const DeepCollectionEquality().equals(other._products, _products)&&(identical(other.loading, loading) || other.loading == loading));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_products),loading);
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'ProductListPageState(products: $products, loading: $loading)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ProductListPageStateCopyWith<$Res> implements $ProductListPageStateCopyWith<$Res> {
|
||||
factory _$ProductListPageStateCopyWith(_ProductListPageState value, $Res Function(_ProductListPageState) _then) = __$ProductListPageStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ProductEntity> products, bool loading
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ProductListPageStateCopyWithImpl<$Res>
|
||||
implements _$ProductListPageStateCopyWith<$Res> {
|
||||
__$ProductListPageStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ProductListPageState _self;
|
||||
final $Res Function(_ProductListPageState) _then;
|
||||
|
||||
/// Create a copy of ProductListPageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? products = null,Object? loading = null,}) {
|
||||
return _then(_ProductListPageState(
|
||||
products: null == products ? _self._products : products // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductEntity>,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:barcode_scanner/backend/api/api_calls.dart';
|
||||
import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart';
|
||||
import 'package:barcode_scanner/backend/objectbox/objectbox_manager.dart';
|
||||
import 'package:barcode_scanner/backend/schema/product/product_struct.dart';
|
||||
import 'package:barcode_scanner/components/loading_progress_component.dart';
|
||||
import 'package:barcode_scanner/components/product_scanned_component.dart';
|
||||
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
|
||||
import 'package:barcode_scanner/themes/app_theme.dart';
|
||||
@ -107,11 +110,14 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
|
||||
debugPrint('Image : ${product["image_url"]}');
|
||||
final productStruct = ProductStruct(
|
||||
id: int.parse(product["id"]),
|
||||
name: product["product_name"],
|
||||
image: product["image_thumb_url"],
|
||||
description: product["categories"],
|
||||
name: product["generic_name"],
|
||||
description: product["product_name"],
|
||||
quantity: product["quantity"],
|
||||
);
|
||||
Box<ProductEntity> productStore = objectboxManager.store
|
||||
.box<ProductEntity>();
|
||||
productStore.put(productStruct.toEntity);
|
||||
//show dialog
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
@ -179,10 +185,13 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
|
||||
final productStruct = ProductStruct(
|
||||
id: int.parse(product["id"]),
|
||||
image: product["image_thumb_url"],
|
||||
name: product["product_name"],
|
||||
description: product["categories"],
|
||||
name: product["generic_name"],
|
||||
description: product["product_name"],
|
||||
quantity: product["quantity"],
|
||||
);
|
||||
Box<ProductEntity> productStore = objectboxManager.store
|
||||
.box<ProductEntity>();
|
||||
productStore.put(productStruct.toEntity);
|
||||
//show dialog
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
@ -234,6 +243,7 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
automaticallyImplyLeading: false,
|
||||
@ -263,7 +273,7 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: loading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
? Center(child: LoadingProgressComponent())
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
|
Loading…
x
Reference in New Issue
Block a user