fix
This commit is contained in:
90
claude.md
90
claude.md
@@ -64,8 +64,8 @@ You have access to these expert subagents - USE THEM PROACTIVELY:
|
||||
## Flutter Best Practices
|
||||
- Use Flutter 3.x features and Material 3 design
|
||||
- Implement clean architecture with Riverpod for state management
|
||||
- Use Hive CE for local database and offline-first functionality
|
||||
- Follow proper dependency injection with GetIt
|
||||
- Use Hive CE for local database with **online-first** strategy (API first, cache fallback)
|
||||
- Follow proper dependency injection with Riverpod providers
|
||||
- Implement proper error handling and user feedback
|
||||
- Follow platform-specific design guidelines
|
||||
- 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
|
||||
|
||||
**Data Requirements**:
|
||||
- Product list from Hive (offline-first)
|
||||
- Product list from API (online-first with Hive cache fallback)
|
||||
- Product images (cached with variants)
|
||||
- Product search indexing
|
||||
- Category relationships
|
||||
@@ -969,45 +969,63 @@ GridView.builder(
|
||||
- Debounce search queries
|
||||
- Optimize cart calculations
|
||||
|
||||
## Offline-First Strategy
|
||||
## Online-First Strategy
|
||||
|
||||
### Data Flow
|
||||
1. **Read**: Always read from Hive first (instant UI)
|
||||
2. **Sync**: Background sync with API when online
|
||||
3. **Update**: Update Hive and UI when sync completes
|
||||
4. **Conflict**: Handle conflicts with last-write-wins strategy
|
||||
1. **Check Connection**: Check if device is online
|
||||
2. **Try API First**: If online, fetch fresh data from API
|
||||
3. **Update Cache**: Save API response to Hive for offline access
|
||||
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
|
||||
@riverpod
|
||||
class DataSync extends _$DataSync {
|
||||
class Products extends _$Products {
|
||||
@override
|
||||
Future<SyncStatus> build() async {
|
||||
return await _performSync();
|
||||
Future<List<Product>> build() async {
|
||||
// Online-first: Try to load from API first
|
||||
final repository = ref.watch(productRepositoryProvider);
|
||||
final networkInfo = ref.watch(networkInfoProvider);
|
||||
|
||||
// Check if online
|
||||
final isConnected = await networkInfo.isConnected;
|
||||
|
||||
if (isConnected) {
|
||||
// Try API first
|
||||
try {
|
||||
final syncResult = await repository.syncProducts();
|
||||
return syncResult.fold(
|
||||
(failure) {
|
||||
// API failed, fallback to cache
|
||||
print('API failed, falling back to cache: ${failure.message}');
|
||||
return _loadFromCache();
|
||||
},
|
||||
(products) => products,
|
||||
);
|
||||
} catch (e) {
|
||||
// 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<SyncStatus> _performSync() async {
|
||||
if (!await ref.read(networkInfoProvider).isConnected) {
|
||||
return SyncStatus.offline;
|
||||
}
|
||||
|
||||
try {
|
||||
// Sync categories first
|
||||
await ref.read(categoriesProvider.notifier).syncCategories();
|
||||
|
||||
// Then sync products and variants
|
||||
await ref.read(productsProvider.notifier).syncProducts();
|
||||
|
||||
// Sync suppliers
|
||||
await ref.read(suppliersProvider.notifier).syncSuppliers();
|
||||
|
||||
// Update last sync time
|
||||
await ref.read(settingsProvider.notifier).updateLastSync();
|
||||
|
||||
return SyncStatus.success;
|
||||
} catch (e) {
|
||||
return SyncStatus.failed;
|
||||
}
|
||||
|
||||
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
|
||||
- [ ] Follows clean architecture principles
|
||||
- [ ] Proper error handling implemented
|
||||
- [ ] Offline-first approach maintained
|
||||
- [ ] **Online-first approach maintained** (API first, cache fallback)
|
||||
- [ ] Performance optimizations applied
|
||||
- [ ] Proper state management with Riverpod
|
||||
- [ ] 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 'network_info_provider.dart';
|
||||
export 'sync_status_provider.dart';
|
||||
export 'dio_client_provider.dart';
|
||||
|
||||
@@ -27,7 +27,7 @@ class EmptyState extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.inbox_outlined,
|
||||
size: 50,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -6,6 +6,7 @@ abstract class CategoryLocalDataSource {
|
||||
Future<List<CategoryModel>> getAllCategories();
|
||||
Future<CategoryModel?> getCategoryById(String id);
|
||||
Future<void> cacheCategories(List<CategoryModel> categories);
|
||||
Future<void> updateCategory(CategoryModel category);
|
||||
Future<void> clearCategories();
|
||||
}
|
||||
|
||||
@@ -30,6 +31,11 @@ class CategoryLocalDataSourceImpl implements CategoryLocalDataSource {
|
||||
await box.putAll(categoryMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateCategory(CategoryModel category) async {
|
||||
await box.put(category.id, category);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCategories() async {
|
||||
await box.clear();
|
||||
|
||||
@@ -18,8 +18,27 @@ class CategoryRepositoryImpl implements CategoryRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Category>>> getAllCategories() async {
|
||||
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());
|
||||
} 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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
@@ -28,11 +47,33 @@ class CategoryRepositoryImpl implements CategoryRepository {
|
||||
@override
|
||||
Future<Either<Failure, Category>> getCategoryById(String id) async {
|
||||
try {
|
||||
final category = await localDataSource.getCategoryById(id);
|
||||
if (category == null) {
|
||||
return Left(NotFoundFailure('Category not found'));
|
||||
}
|
||||
// Try remote first (online-first)
|
||||
final category = await remoteDataSource.getCategoryById(id);
|
||||
// Cache the result
|
||||
await localDataSource.updateCategory(category);
|
||||
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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import '../../../../core/providers/providers.dart';
|
||||
|
||||
part 'categories_provider.g.dart';
|
||||
|
||||
/// Provider for categories list with API-first approach
|
||||
/// Provider for categories list with online-first approach
|
||||
@riverpod
|
||||
class Categories extends _$Categories {
|
||||
@override
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
onRetry: () => ref.refresh(productsProvider),
|
||||
),
|
||||
data: (paginationState) {
|
||||
final products = paginationState.products;
|
||||
data: (products) {
|
||||
|
||||
if (products.isEmpty) {
|
||||
return const EmptyState(
|
||||
|
||||
@@ -13,7 +13,7 @@ class ProductModel extends HiveObject {
|
||||
final String name;
|
||||
|
||||
@HiveField(2)
|
||||
final String description;
|
||||
final String? description;
|
||||
|
||||
@HiveField(3)
|
||||
final double price;
|
||||
@@ -39,7 +39,7 @@ class ProductModel extends HiveObject {
|
||||
ProductModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
this.description,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
required this.categoryId,
|
||||
@@ -86,7 +86,7 @@ class ProductModel extends HiveObject {
|
||||
return ProductModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
description: json['description'] as String?,
|
||||
price: (json['price'] as num).toDouble(),
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
categoryId: json['categoryId'] as String,
|
||||
|
||||
@@ -19,8 +19,27 @@ class ProductRepositoryImpl implements ProductRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> getAllProducts() async {
|
||||
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());
|
||||
} 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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
@@ -29,9 +48,29 @@ class ProductRepositoryImpl implements ProductRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> getProductsByCategory(String categoryId) async {
|
||||
try {
|
||||
final allProducts = await localDataSource.getAllProducts();
|
||||
final filtered = allProducts.where((p) => p.categoryId == categoryId).toList();
|
||||
return Right(filtered.map((model) => model.toEntity()).toList());
|
||||
// Try remote first (online-first)
|
||||
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());
|
||||
} 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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
@@ -40,13 +79,37 @@ class ProductRepositoryImpl implements ProductRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
|
||||
try {
|
||||
final allProducts = await localDataSource.getAllProducts();
|
||||
final filtered = allProducts.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());
|
||||
// Try remote first (online-first)
|
||||
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 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(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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
@@ -55,11 +118,33 @@ class ProductRepositoryImpl implements ProductRepository {
|
||||
@override
|
||||
Future<Either<Failure, Product>> getProductById(String id) async {
|
||||
try {
|
||||
final product = await localDataSource.getProductById(id);
|
||||
if (product == null) {
|
||||
return Left(NotFoundFailure('Product not found'));
|
||||
}
|
||||
// Try remote first (online-first)
|
||||
final product = await remoteDataSource.getProductById(id);
|
||||
// Cache the result
|
||||
await localDataSource.updateProduct(product);
|
||||
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) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
@@ -68,11 +153,7 @@ class ProductRepositoryImpl implements ProductRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Product>>> syncProducts() async {
|
||||
try {
|
||||
final response = await remoteDataSource.getAllProducts();
|
||||
final productsData = response['data'] as List<dynamic>;
|
||||
final products = productsData
|
||||
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
final products = await remoteDataSource.getAllProducts();
|
||||
await localDataSource.cacheProducts(products);
|
||||
final entities = products.map((model) => model.toEntity()).toList();
|
||||
return Right(entities);
|
||||
|
||||
@@ -246,7 +246,7 @@ class ProductDetailPage extends ConsumerWidget {
|
||||
|
||||
/// Build description section
|
||||
Widget _buildDescriptionSection(BuildContext context) {
|
||||
if (product.description.isEmpty) {
|
||||
if (product.description == null || product.description!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ class ProductDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
product.description,
|
||||
product.description!,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@ class FilteredProducts extends _$FilteredProducts {
|
||||
// Watch products state
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
final products = productsAsync.when(
|
||||
data: (data) => data.products,
|
||||
data: (data) => data,
|
||||
loading: () => <Product>[],
|
||||
error: (_, __) => <Product>[],
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ final class FilteredProductsProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$filteredProductsHash() => r'd8ca6d80a71bf354e3afe6c38335996a8bfc74b7';
|
||||
String _$filteredProductsHash() => r'97fb09ade4bc65f92f3d4844b059bb2b0660d3df';
|
||||
|
||||
/// Filtered products provider
|
||||
/// 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';
|
||||
|
||||
/// Provider for products list with API-first approach
|
||||
/// Provider for products list with online-first approach
|
||||
@riverpod
|
||||
class Products extends _$Products {
|
||||
@override
|
||||
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 networkInfo = ref.watch(networkInfoProvider);
|
||||
|
||||
|
||||
@@ -85,9 +85,9 @@ class ProductListItem extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (product.description.isNotEmpty)
|
||||
if (product.description != null && product.description!.isNotEmpty)
|
||||
Text(
|
||||
product.description,
|
||||
product.description!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user