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

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