fix
This commit is contained in:
82
claude.md
82
claude.md
@@ -64,8 +64,8 @@ You have access to these expert subagents - USE THEM PROACTIVELY:
|
|||||||
## Flutter Best Practices
|
## Flutter Best Practices
|
||||||
- Use Flutter 3.x features and Material 3 design
|
- Use Flutter 3.x features and Material 3 design
|
||||||
- Implement clean architecture with Riverpod for state management
|
- Implement clean architecture with Riverpod for state management
|
||||||
- Use Hive CE for local database and offline-first functionality
|
- Use Hive CE for local database with **online-first** strategy (API first, cache fallback)
|
||||||
- Follow proper dependency injection with GetIt
|
- Follow proper dependency injection with Riverpod providers
|
||||||
- Implement proper error handling and user feedback
|
- Implement proper error handling and user feedback
|
||||||
- Follow platform-specific design guidelines
|
- Follow platform-specific design guidelines
|
||||||
- Use proper localization for multi-language support
|
- Use proper localization for multi-language support
|
||||||
@@ -453,7 +453,7 @@ A comprehensive Flutter-based Point of Sale (POS) application designed for retai
|
|||||||
- Supplier state
|
- Supplier state
|
||||||
|
|
||||||
**Data Requirements**:
|
**Data Requirements**:
|
||||||
- Product list from Hive (offline-first)
|
- Product list from API (online-first with Hive cache fallback)
|
||||||
- Product images (cached with variants)
|
- Product images (cached with variants)
|
||||||
- Product search indexing
|
- Product search indexing
|
||||||
- Category relationships
|
- Category relationships
|
||||||
@@ -969,45 +969,63 @@ GridView.builder(
|
|||||||
- Debounce search queries
|
- Debounce search queries
|
||||||
- Optimize cart calculations
|
- Optimize cart calculations
|
||||||
|
|
||||||
## Offline-First Strategy
|
## Online-First Strategy
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
1. **Read**: Always read from Hive first (instant UI)
|
1. **Check Connection**: Check if device is online
|
||||||
2. **Sync**: Background sync with API when online
|
2. **Try API First**: If online, fetch fresh data from API
|
||||||
3. **Update**: Update Hive and UI when sync completes
|
3. **Update Cache**: Save API response to Hive for offline access
|
||||||
4. **Conflict**: Handle conflicts with last-write-wins strategy
|
4. **Fallback to Cache**: If API fails or offline, load from Hive
|
||||||
|
5. **Show Data**: Display data to user (from API or cache)
|
||||||
|
|
||||||
### Sync Logic
|
### Implementation Pattern
|
||||||
```dart
|
```dart
|
||||||
@riverpod
|
@riverpod
|
||||||
class DataSync extends _$DataSync {
|
class Products extends _$Products {
|
||||||
@override
|
@override
|
||||||
Future<SyncStatus> build() async {
|
Future<List<Product>> build() async {
|
||||||
return await _performSync();
|
// Online-first: Try to load from API first
|
||||||
}
|
final repository = ref.watch(productRepositoryProvider);
|
||||||
|
final networkInfo = ref.watch(networkInfoProvider);
|
||||||
|
|
||||||
Future<SyncStatus> _performSync() async {
|
// Check if online
|
||||||
if (!await ref.read(networkInfoProvider).isConnected) {
|
final isConnected = await networkInfo.isConnected;
|
||||||
return SyncStatus.offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isConnected) {
|
||||||
|
// Try API first
|
||||||
try {
|
try {
|
||||||
// Sync categories first
|
final syncResult = await repository.syncProducts();
|
||||||
await ref.read(categoriesProvider.notifier).syncCategories();
|
return syncResult.fold(
|
||||||
|
(failure) {
|
||||||
// Then sync products and variants
|
// API failed, fallback to cache
|
||||||
await ref.read(productsProvider.notifier).syncProducts();
|
print('API failed, falling back to cache: ${failure.message}');
|
||||||
|
return _loadFromCache();
|
||||||
// Sync suppliers
|
},
|
||||||
await ref.read(suppliersProvider.notifier).syncSuppliers();
|
(products) => products,
|
||||||
|
);
|
||||||
// Update last sync time
|
|
||||||
await ref.read(settingsProvider.notifier).updateLastSync();
|
|
||||||
|
|
||||||
return SyncStatus.success;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return SyncStatus.failed;
|
// API error, fallback to cache
|
||||||
|
print('API error, falling back to cache: $e');
|
||||||
|
return _loadFromCache();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Offline, load from cache
|
||||||
|
print('Offline, loading from cache');
|
||||||
|
return _loadFromCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Product>> _loadFromCache() async {
|
||||||
|
final repository = ref.read(productRepositoryProvider);
|
||||||
|
final result = await repository.getAllProducts();
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
print('Cache load failed: ${failure.message}');
|
||||||
|
return <Product>[];
|
||||||
|
},
|
||||||
|
(products) => products,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1134,7 +1152,7 @@ class DataSync extends _$DataSync {
|
|||||||
### Code Review Checklist
|
### Code Review Checklist
|
||||||
- [ ] Follows clean architecture principles
|
- [ ] Follows clean architecture principles
|
||||||
- [ ] Proper error handling implemented
|
- [ ] Proper error handling implemented
|
||||||
- [ ] Offline-first approach maintained
|
- [ ] **Online-first approach maintained** (API first, cache fallback)
|
||||||
- [ ] Performance optimizations applied
|
- [ ] Performance optimizations applied
|
||||||
- [ ] Proper state management with Riverpod
|
- [ ] Proper state management with Riverpod
|
||||||
- [ ] Hive models and adapters properly defined
|
- [ ] Hive models and adapters properly defined
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import '../network/dio_client.dart';
|
|
||||||
|
|
||||||
part 'dio_client_provider.g.dart';
|
|
||||||
|
|
||||||
/// Provider for DioClient singleton
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
DioClient dioClient(Ref ref) {
|
|
||||||
return DioClient();
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'dio_client_provider.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
// ignore_for_file: type=lint, type=warning
|
|
||||||
/// Provider for DioClient singleton
|
|
||||||
|
|
||||||
@ProviderFor(dioClient)
|
|
||||||
const dioClientProvider = DioClientProvider._();
|
|
||||||
|
|
||||||
/// Provider for DioClient singleton
|
|
||||||
|
|
||||||
final class DioClientProvider
|
|
||||||
extends $FunctionalProvider<DioClient, DioClient, DioClient>
|
|
||||||
with $Provider<DioClient> {
|
|
||||||
/// Provider for DioClient singleton
|
|
||||||
const DioClientProvider._()
|
|
||||||
: super(
|
|
||||||
from: null,
|
|
||||||
argument: null,
|
|
||||||
retry: null,
|
|
||||||
name: r'dioClientProvider',
|
|
||||||
isAutoDispose: false,
|
|
||||||
dependencies: null,
|
|
||||||
$allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String debugGetCreateSourceHash() => _$dioClientHash();
|
|
||||||
|
|
||||||
@$internal
|
|
||||||
@override
|
|
||||||
$ProviderElement<DioClient> $createElement($ProviderPointer pointer) =>
|
|
||||||
$ProviderElement(pointer);
|
|
||||||
|
|
||||||
@override
|
|
||||||
DioClient create(Ref ref) {
|
|
||||||
return dioClient(ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@macro riverpod.override_with_value}
|
|
||||||
Override overrideWithValue(DioClient value) {
|
|
||||||
return $ProviderOverride(
|
|
||||||
origin: this,
|
|
||||||
providerOverride: $SyncValueProvider<DioClient>(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';
|
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
export 'core_providers.dart';
|
export 'core_providers.dart';
|
||||||
export 'network_info_provider.dart';
|
export 'network_info_provider.dart';
|
||||||
export 'sync_status_provider.dart';
|
export 'sync_status_provider.dart';
|
||||||
export 'dio_client_provider.dart';
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class EmptyState extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon ?? Icons.inbox_outlined,
|
icon ?? Icons.inbox_outlined,
|
||||||
size: 50,
|
size: 48,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ abstract class CategoryLocalDataSource {
|
|||||||
Future<List<CategoryModel>> getAllCategories();
|
Future<List<CategoryModel>> getAllCategories();
|
||||||
Future<CategoryModel?> getCategoryById(String id);
|
Future<CategoryModel?> getCategoryById(String id);
|
||||||
Future<void> cacheCategories(List<CategoryModel> categories);
|
Future<void> cacheCategories(List<CategoryModel> categories);
|
||||||
|
Future<void> updateCategory(CategoryModel category);
|
||||||
Future<void> clearCategories();
|
Future<void> clearCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,11 @@ class CategoryLocalDataSourceImpl implements CategoryLocalDataSource {
|
|||||||
await box.putAll(categoryMap);
|
await box.putAll(categoryMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateCategory(CategoryModel category) async {
|
||||||
|
await box.put(category.id, category);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> clearCategories() async {
|
Future<void> clearCategories() async {
|
||||||
await box.clear();
|
await box.clear();
|
||||||
|
|||||||
@@ -18,8 +18,27 @@ class CategoryRepositoryImpl implements CategoryRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Category>>> getAllCategories() async {
|
Future<Either<Failure, List<Category>>> getAllCategories() async {
|
||||||
try {
|
try {
|
||||||
final categories = await localDataSource.getAllCategories();
|
// Try remote first (online-first)
|
||||||
|
final categories = await remoteDataSource.getAllCategories();
|
||||||
|
// Cache the results
|
||||||
|
await localDataSource.cacheCategories(categories);
|
||||||
return Right(categories.map((model) => model.toEntity()).toList());
|
return Right(categories.map((model) => model.toEntity()).toList());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedCategories = await localDataSource.getAllCategories();
|
||||||
|
return Right(cachedCategories.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedCategories = await localDataSource.getAllCategories();
|
||||||
|
return Right(cachedCategories.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
@@ -28,11 +47,33 @@ class CategoryRepositoryImpl implements CategoryRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, Category>> getCategoryById(String id) async {
|
Future<Either<Failure, Category>> getCategoryById(String id) async {
|
||||||
try {
|
try {
|
||||||
final category = await localDataSource.getCategoryById(id);
|
// Try remote first (online-first)
|
||||||
if (category == null) {
|
final category = await remoteDataSource.getCategoryById(id);
|
||||||
return Left(NotFoundFailure('Category not found'));
|
// Cache the result
|
||||||
}
|
await localDataSource.updateCategory(category);
|
||||||
return Right(category.toEntity());
|
return Right(category.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedCategory = await localDataSource.getCategoryById(id);
|
||||||
|
if (cachedCategory == null) {
|
||||||
|
return Left(NotFoundFailure('Category not found in cache'));
|
||||||
|
}
|
||||||
|
return Right(cachedCategory.toEntity());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedCategory = await localDataSource.getCategoryById(id);
|
||||||
|
if (cachedCategory == null) {
|
||||||
|
return Left(NotFoundFailure('Category not found in cache'));
|
||||||
|
}
|
||||||
|
return Right(cachedCategory.toEntity());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import '../../../../core/providers/providers.dart';
|
|||||||
|
|
||||||
part 'categories_provider.g.dart';
|
part 'categories_provider.g.dart';
|
||||||
|
|
||||||
/// Provider for categories list with API-first approach
|
/// Provider for categories list with online-first approach
|
||||||
@riverpod
|
@riverpod
|
||||||
class Categories extends _$Categories {
|
class Categories extends _$Categories {
|
||||||
@override
|
@override
|
||||||
Future<List<Category>> build() async {
|
Future<List<Category>> build() async {
|
||||||
// API-first: Try to load from API first
|
// Online-first: Try to load from API first
|
||||||
final repository = ref.watch(categoryRepositoryProvider);
|
final repository = ref.watch(categoryRepositoryProvider);
|
||||||
final networkInfo = ref.watch(networkInfoProvider);
|
final networkInfo = ref.watch(networkInfoProvider);
|
||||||
|
|
||||||
@@ -90,18 +90,3 @@ class Categories extends _$Categories {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider for selected category
|
|
||||||
@riverpod
|
|
||||||
class SelectedCategory extends _$SelectedCategory {
|
|
||||||
@override
|
|
||||||
String? build() => null;
|
|
||||||
|
|
||||||
void select(String? categoryId) {
|
|
||||||
state = categoryId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
state = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -58,62 +58,3 @@ abstract class _$Categories extends $AsyncNotifier<List<Category>> {
|
|||||||
element.handleValue(ref, created);
|
element.handleValue(ref, created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider for selected category
|
|
||||||
|
|
||||||
@ProviderFor(SelectedCategory)
|
|
||||||
const selectedCategoryProvider = SelectedCategoryProvider._();
|
|
||||||
|
|
||||||
/// Provider for selected category
|
|
||||||
final class SelectedCategoryProvider
|
|
||||||
extends $NotifierProvider<SelectedCategory, String?> {
|
|
||||||
/// Provider for selected category
|
|
||||||
const SelectedCategoryProvider._()
|
|
||||||
: super(
|
|
||||||
from: null,
|
|
||||||
argument: null,
|
|
||||||
retry: null,
|
|
||||||
name: r'selectedCategoryProvider',
|
|
||||||
isAutoDispose: true,
|
|
||||||
dependencies: null,
|
|
||||||
$allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String debugGetCreateSourceHash() => _$selectedCategoryHash();
|
|
||||||
|
|
||||||
@$internal
|
|
||||||
@override
|
|
||||||
SelectedCategory create() => SelectedCategory();
|
|
||||||
|
|
||||||
/// {@macro riverpod.override_with_value}
|
|
||||||
Override overrideWithValue(String? value) {
|
|
||||||
return $ProviderOverride(
|
|
||||||
origin: this,
|
|
||||||
providerOverride: $SyncValueProvider<String?>(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _$selectedCategoryHash() => r'a47cd2de07ad285d4b73b2294ba954cb1cdd8e4c';
|
|
||||||
|
|
||||||
/// Provider for selected category
|
|
||||||
|
|
||||||
abstract class _$SelectedCategory extends $Notifier<String?> {
|
|
||||||
String? build();
|
|
||||||
@$mustCallSuper
|
|
||||||
@override
|
|
||||||
void runBuild() {
|
|
||||||
final created = build();
|
|
||||||
final ref = this.ref as $Ref<String?, String?>;
|
|
||||||
final element =
|
|
||||||
ref.element
|
|
||||||
as $ClassProviderElement<
|
|
||||||
AnyNotifier<String?, String?>,
|
|
||||||
String?,
|
|
||||||
Object?,
|
|
||||||
Object?
|
|
||||||
>;
|
|
||||||
element.handleValue(ref, created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ class ProductSelector extends ConsumerWidget {
|
|||||||
message: error.toString(),
|
message: error.toString(),
|
||||||
onRetry: () => ref.refresh(productsProvider),
|
onRetry: () => ref.refresh(productsProvider),
|
||||||
),
|
),
|
||||||
data: (paginationState) {
|
data: (products) {
|
||||||
final products = paginationState.products;
|
|
||||||
|
|
||||||
if (products.isEmpty) {
|
if (products.isEmpty) {
|
||||||
return const EmptyState(
|
return const EmptyState(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ProductModel extends HiveObject {
|
|||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
@HiveField(2)
|
@HiveField(2)
|
||||||
final String description;
|
final String? description;
|
||||||
|
|
||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
final double price;
|
final double price;
|
||||||
@@ -39,7 +39,7 @@ class ProductModel extends HiveObject {
|
|||||||
ProductModel({
|
ProductModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
this.description,
|
||||||
required this.price,
|
required this.price,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
required this.categoryId,
|
required this.categoryId,
|
||||||
@@ -86,7 +86,7 @@ class ProductModel extends HiveObject {
|
|||||||
return ProductModel(
|
return ProductModel(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
description: json['description'] as String? ?? '',
|
description: json['description'] as String?,
|
||||||
price: (json['price'] as num).toDouble(),
|
price: (json['price'] as num).toDouble(),
|
||||||
imageUrl: json['imageUrl'] as String?,
|
imageUrl: json['imageUrl'] as String?,
|
||||||
categoryId: json['categoryId'] as String,
|
categoryId: json['categoryId'] as String,
|
||||||
|
|||||||
@@ -19,8 +19,27 @@ class ProductRepositoryImpl implements ProductRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Product>>> getAllProducts() async {
|
Future<Either<Failure, List<Product>>> getAllProducts() async {
|
||||||
try {
|
try {
|
||||||
final products = await localDataSource.getAllProducts();
|
// Try remote first (online-first)
|
||||||
|
final products = await remoteDataSource.getAllProducts();
|
||||||
|
// Cache the results
|
||||||
|
await localDataSource.cacheProducts(products);
|
||||||
return Right(products.map((model) => model.toEntity()).toList());
|
return Right(products.map((model) => model.toEntity()).toList());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
return Right(cachedProducts.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
return Right(cachedProducts.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
@@ -29,9 +48,29 @@ class ProductRepositoryImpl implements ProductRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Product>>> getProductsByCategory(String categoryId) async {
|
Future<Either<Failure, List<Product>>> getProductsByCategory(String categoryId) async {
|
||||||
try {
|
try {
|
||||||
final allProducts = await localDataSource.getAllProducts();
|
// Try remote first (online-first)
|
||||||
final filtered = allProducts.where((p) => p.categoryId == categoryId).toList();
|
final allProducts = await remoteDataSource.getAllProducts(categoryId: categoryId);
|
||||||
|
// Cache the results
|
||||||
|
await localDataSource.cacheProducts(allProducts);
|
||||||
|
return Right(allProducts.map((model) => model.toEntity()).toList());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
final filtered = cachedProducts.where((p) => p.categoryId == categoryId).toList();
|
||||||
return Right(filtered.map((model) => model.toEntity()).toList());
|
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
final filtered = cachedProducts.where((p) => p.categoryId == categoryId).toList();
|
||||||
|
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
@@ -40,13 +79,37 @@ class ProductRepositoryImpl implements ProductRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
|
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
|
||||||
try {
|
try {
|
||||||
final allProducts = await localDataSource.getAllProducts();
|
// Try remote first (online-first)
|
||||||
final filtered = allProducts.where((p) {
|
final searchResults = await remoteDataSource.getAllProducts(search: query);
|
||||||
|
// Cache the results
|
||||||
|
await localDataSource.cacheProducts(searchResults);
|
||||||
|
return Right(searchResults.map((model) => model.toEntity()).toList());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, search in local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
final filtered = cachedProducts.where((p) {
|
||||||
final nameMatch = p.name.toLowerCase().contains(query.toLowerCase());
|
final nameMatch = p.name.toLowerCase().contains(query.toLowerCase());
|
||||||
final descMatch = p.description?.toLowerCase().contains(query.toLowerCase()) ?? false;
|
final descMatch = p.description?.toLowerCase().contains(query.toLowerCase()) ?? false;
|
||||||
return nameMatch || descMatch;
|
return nameMatch || descMatch;
|
||||||
}).toList();
|
}).toList();
|
||||||
return Right(filtered.map((model) => model.toEntity()).toList());
|
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, search in local cache
|
||||||
|
try {
|
||||||
|
final cachedProducts = await localDataSource.getAllProducts();
|
||||||
|
final filtered = cachedProducts.where((p) {
|
||||||
|
final nameMatch = p.name.toLowerCase().contains(query.toLowerCase());
|
||||||
|
final descMatch = p.description?.toLowerCase().contains(query.toLowerCase()) ?? false;
|
||||||
|
return nameMatch || descMatch;
|
||||||
|
}).toList();
|
||||||
|
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
@@ -55,11 +118,33 @@ class ProductRepositoryImpl implements ProductRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, Product>> getProductById(String id) async {
|
Future<Either<Failure, Product>> getProductById(String id) async {
|
||||||
try {
|
try {
|
||||||
final product = await localDataSource.getProductById(id);
|
// Try remote first (online-first)
|
||||||
if (product == null) {
|
final product = await remoteDataSource.getProductById(id);
|
||||||
return Left(NotFoundFailure('Product not found'));
|
// Cache the result
|
||||||
}
|
await localDataSource.updateProduct(product);
|
||||||
return Right(product.toEntity());
|
return Right(product.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Remote failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProduct = await localDataSource.getProductById(id);
|
||||||
|
if (cachedProduct == null) {
|
||||||
|
return Left(NotFoundFailure('Product not found in cache'));
|
||||||
|
}
|
||||||
|
return Right(cachedProduct.toEntity());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(ServerFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// Network failed, try local cache
|
||||||
|
try {
|
||||||
|
final cachedProduct = await localDataSource.getProductById(id);
|
||||||
|
if (cachedProduct == null) {
|
||||||
|
return Left(NotFoundFailure('Product not found in cache'));
|
||||||
|
}
|
||||||
|
return Right(cachedProduct.toEntity());
|
||||||
|
} on CacheException catch (cacheError) {
|
||||||
|
return Left(NetworkFailure('${e.message}. Cache also unavailable.'));
|
||||||
|
}
|
||||||
} on CacheException catch (e) {
|
} on CacheException catch (e) {
|
||||||
return Left(CacheFailure(e.message));
|
return Left(CacheFailure(e.message));
|
||||||
}
|
}
|
||||||
@@ -68,11 +153,7 @@ class ProductRepositoryImpl implements ProductRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Product>>> syncProducts() async {
|
Future<Either<Failure, List<Product>>> syncProducts() async {
|
||||||
try {
|
try {
|
||||||
final response = await remoteDataSource.getAllProducts();
|
final products = await remoteDataSource.getAllProducts();
|
||||||
final productsData = response['data'] as List<dynamic>;
|
|
||||||
final products = productsData
|
|
||||||
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
await localDataSource.cacheProducts(products);
|
await localDataSource.cacheProducts(products);
|
||||||
final entities = products.map((model) => model.toEntity()).toList();
|
final entities = products.map((model) => model.toEntity()).toList();
|
||||||
return Right(entities);
|
return Right(entities);
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
/// Build description section
|
/// Build description section
|
||||||
Widget _buildDescriptionSection(BuildContext context) {
|
Widget _buildDescriptionSection(BuildContext context) {
|
||||||
if (product.description.isEmpty) {
|
if (product.description == null || product.description!.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ class ProductDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
product.description,
|
product.description!,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FilteredProducts extends _$FilteredProducts {
|
|||||||
// Watch products state
|
// Watch products state
|
||||||
final productsAsync = ref.watch(productsProvider);
|
final productsAsync = ref.watch(productsProvider);
|
||||||
final products = productsAsync.when(
|
final products = productsAsync.when(
|
||||||
data: (data) => data.products,
|
data: (data) => data,
|
||||||
loading: () => <Product>[],
|
loading: () => <Product>[],
|
||||||
error: (_, __) => <Product>[],
|
error: (_, __) => <Product>[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ final class FilteredProductsProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$filteredProductsHash() => r'd8ca6d80a71bf354e3afe6c38335996a8bfc74b7';
|
String _$filteredProductsHash() => r'97fb09ade4bc65f92f3d4844b059bb2b0660d3df';
|
||||||
|
|
||||||
/// Filtered products provider
|
/// Filtered products provider
|
||||||
/// Combines products, search query, and category filter to provide filtered results
|
/// Combines products, search query, and category filter to provide filtered results
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import '../../../../core/providers/providers.dart';
|
|||||||
|
|
||||||
part 'products_provider.g.dart';
|
part 'products_provider.g.dart';
|
||||||
|
|
||||||
/// Provider for products list with API-first approach
|
/// Provider for products list with online-first approach
|
||||||
@riverpod
|
@riverpod
|
||||||
class Products extends _$Products {
|
class Products extends _$Products {
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> build() async {
|
Future<List<Product>> build() async {
|
||||||
// API-first: Try to load from API first
|
// Online-first: Try to load from API first
|
||||||
final repository = ref.watch(productRepositoryProvider);
|
final repository = ref.watch(productRepositoryProvider);
|
||||||
final networkInfo = ref.watch(networkInfoProvider);
|
final networkInfo = ref.watch(networkInfoProvider);
|
||||||
|
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ class ProductListItem extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (product.description.isNotEmpty)
|
if (product.description != null && product.description!.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
product.description,
|
product.description!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user