This commit is contained in:
2025-10-16 17:22:27 +07:00
parent 3b1f198f2a
commit 7dc66d80fc
17 changed files with 222 additions and 217 deletions

View File

@@ -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();

View File

@@ -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));
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,
),
],

View File

@@ -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>[],
);

View File

@@ -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

View File

@@ -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);

View File

@@ -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,
),