This commit is contained in:
2025-10-10 22:49:05 +07:00
parent 02941e2234
commit 38c16bf0b9
49 changed files with 2702 additions and 740 deletions

View File

@@ -1,12 +1,42 @@
import 'package:dio/dio.dart';
import '../models/product_model.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/network/api_response.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
/// Product remote data source using API
abstract class ProductRemoteDataSource {
Future<List<ProductModel>> getAllProducts();
/// Get all products with pagination and filters
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
});
/// Get single product by ID
Future<ProductModel> getProductById(String id);
Future<List<ProductModel>> searchProducts(String query);
/// Search products by query with pagination
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> searchProducts(
String query,
int page,
int limit,
);
/// Get products by category with pagination
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> getProductsByCategory(
String categoryId,
int page,
int limit,
);
}
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
@@ -15,25 +45,198 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
ProductRemoteDataSourceImpl(this.client);
@override
Future<List<ProductModel>> getAllProducts() async {
final response = await client.get(ApiConstants.products);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<Map<String, dynamic>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
// Add optional filters
if (categoryId != null) queryParams['categoryId'] = categoryId;
if (search != null) queryParams['search'] = search;
if (minPrice != null) queryParams['minPrice'] = minPrice;
if (maxPrice != null) queryParams['maxPrice'] = maxPrice;
if (isAvailable != null) queryParams['isAvailable'] = isAvailable;
final response = await client.get(
ApiConstants.products,
queryParameters: queryParams,
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch products',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch products: $e');
}
}
@override
Future<ProductModel> getProductById(String id) async {
final response = await client.get(ApiConstants.productById(id));
return ProductModel.fromJson(response.data);
try {
final response = await client.get(ApiConstants.productById(id));
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<ProductModel>.fromJson(
response.data as Map<String, dynamic>,
(data) => ProductModel.fromJson(data as Map<String, dynamic>),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch product',
);
}
return apiResponse.data;
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch product: $e');
}
}
@override
Future<List<ProductModel>> searchProducts(String query) async {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {'q': query},
);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<Map<String, dynamic>> searchProducts(
String query,
int page,
int limit,
) async {
try {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {
'q': query,
'page': page,
'limit': limit,
},
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to search products',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to search products: $e');
}
}
@override
Future<Map<String, dynamic>> getProductsByCategory(
String categoryId,
int page,
int limit,
) async {
try {
final response = await client.get(
ApiConstants.productsByCategory(categoryId),
queryParameters: {
'page': page,
'limit': limit,
},
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch products by category',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch products by category: $e');
}
}
/// Handle Dio errors and convert to custom exceptions
Exception _handleDioError(DioException error) {
switch (error.response?.statusCode) {
case ApiConstants.statusBadRequest:
return ValidationException(
error.response?.data['message'] ?? 'Invalid request',
);
case ApiConstants.statusUnauthorized:
return UnauthorizedException(
error.response?.data['message'] ?? 'Unauthorized access',
);
case ApiConstants.statusForbidden:
return UnauthorizedException(
error.response?.data['message'] ?? 'Access forbidden',
);
case ApiConstants.statusNotFound:
return NotFoundException(
error.response?.data['message'] ?? 'Product not found',
);
case ApiConstants.statusInternalServerError:
case ApiConstants.statusBadGateway:
case ApiConstants.statusServiceUnavailable:
return ServerException(
error.response?.data['message'] ?? 'Server error',
);
default:
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout) {
return NetworkException('Connection timeout');
} else if (error.type == DioExceptionType.connectionError) {
return NetworkException('No internet connection');
}
return ServerException('Unexpected error occurred');
}
}
}

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,
@@ -83,18 +83,25 @@ class ProductModel extends HiveObject {
/// Create from JSON
factory ProductModel.fromJson(Map<String, dynamic> json) {
// Handle price as string or number from API
final priceValue = json['price'];
final price = priceValue is String
? double.parse(priceValue)
: (priceValue as num).toDouble();
return ProductModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
price: (json['price'] as num).toDouble(),
description: json['description'] as String?,
price: price,
imageUrl: json['imageUrl'] as String?,
categoryId: json['categoryId'] as String,
stockQuantity: json['stockQuantity'] as int,
isAvailable: json['isAvailable'] as bool,
stockQuantity: json['stockQuantity'] as int? ?? 0,
isAvailable: json['isAvailable'] as bool? ?? true,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
// Note: Nested 'category' object is ignored as we only need categoryId
}
/// Convert to JSON

View File

@@ -19,7 +19,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
return ProductModel(
id: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String,
description: fields[2] as String?,
price: (fields[3] as num).toDouble(),
imageUrl: fields[4] as String?,
categoryId: fields[5] as String,

View File

@@ -3,6 +3,7 @@ import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../datasources/product_local_datasource.dart';
import '../datasources/product_remote_datasource.dart';
import '../models/product_model.dart';
import '../../../../core/errors/failures.dart';
import '../../../../core/errors/exceptions.dart';
@@ -40,10 +41,11 @@ class ProductRepositoryImpl implements ProductRepository {
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
try {
final allProducts = await localDataSource.getAllProducts();
final filtered = allProducts.where((p) =>
p.name.toLowerCase().contains(query.toLowerCase()) ||
p.description.toLowerCase().contains(query.toLowerCase())
).toList();
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());
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
@@ -66,9 +68,14 @@ class ProductRepositoryImpl implements ProductRepository {
@override
Future<Either<Failure, List<Product>>> syncProducts() async {
try {
final products = await remoteDataSource.getAllProducts();
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();
await localDataSource.cacheProducts(products);
return Right(products.map((model) => model.toEntity()).toList());
final entities = products.map((model) => model.toEntity()).toList();
return Right(entities);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {

View File

@@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
class Product extends Equatable {
final String id;
final String name;
final String description;
final String? description;
final double price;
final String? imageUrl;
final String categoryId;
@@ -16,7 +16,7 @@ class Product extends Equatable {
const Product({
required this.id,
required this.name,
required this.description,
this.description,
required this.price,
this.imageUrl,
required this.categoryId,

View File

@@ -27,7 +27,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
// Get filtered products from the provider
final filteredProducts = productsAsync.when(
data: (products) => products,
data: (paginationState) => paginationState.products,
loading: () => <Product>[],
error: (_, __) => <Product>[],
);
@@ -170,8 +170,8 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
),
body: RefreshIndicator(
onRefresh: () async {
await ref.refresh(productsProvider.future);
await ref.refresh(categoriesProvider.future);
ref.read(productsProvider.notifier).refresh();
ref.read(categoriesProvider.notifier).refresh();
},
child: Column(
children: [

View File

@@ -1,36 +1,37 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/product.dart';
import 'products_provider.dart';
import 'search_query_provider.dart' as search_providers;
import 'selected_category_provider.dart';
part 'filtered_products_provider.g.dart';
/// Filtered products provider
/// Combines products, search query, and category filter to provide filtered results
/// This provider works on the client-side for additional filtering after API fetches
@riverpod
class FilteredProducts extends _$FilteredProducts {
@override
List<Product> build() {
// Watch all products
// Watch products state
final productsAsync = ref.watch(productsProvider);
final products = productsAsync.when(
data: (data) => data,
data: (data) => data.products,
loading: () => <Product>[],
error: (_, __) => <Product>[],
);
// Watch search query
final searchQuery = ref.watch(search_providers.searchQueryProvider);
final searchQuery = ref.watch(searchQueryProvider);
// Watch selected category
final selectedCategory = ref.watch(selectedCategoryProvider);
// Apply filters
// Apply client-side filters (additional to API filters)
return _applyFilters(products, searchQuery, selectedCategory);
}
/// Apply search and category filters to products
/// This is client-side filtering for real-time updates
List<Product> _applyFilters(
List<Product> products,
String searchQuery,
@@ -48,7 +49,7 @@ class FilteredProducts extends _$FilteredProducts {
final lowerQuery = searchQuery.toLowerCase();
filtered = filtered.where((p) {
return p.name.toLowerCase().contains(lowerQuery) ||
p.description.toLowerCase().contains(lowerQuery);
(p.description?.toLowerCase().contains(lowerQuery) ?? false);
}).toList();
}

View File

@@ -10,16 +10,19 @@ part of 'filtered_products_provider.dart';
// ignore_for_file: type=lint, type=warning
/// Filtered products provider
/// Combines products, search query, and category filter to provide filtered results
/// This provider works on the client-side for additional filtering after API fetches
@ProviderFor(FilteredProducts)
const filteredProductsProvider = FilteredProductsProvider._();
/// Filtered products provider
/// Combines products, search query, and category filter to provide filtered results
/// This provider works on the client-side for additional filtering after API fetches
final class FilteredProductsProvider
extends $NotifierProvider<FilteredProducts, List<Product>> {
/// Filtered products provider
/// Combines products, search query, and category filter to provide filtered results
/// This provider works on the client-side for additional filtering after API fetches
const FilteredProductsProvider._()
: super(
from: null,
@@ -47,10 +50,11 @@ final class FilteredProductsProvider
}
}
String _$filteredProductsHash() => r'04d66ed1cb868008cf3e6aba6571f7928a48e814';
String _$filteredProductsHash() => r'd8ca6d80a71bf354e3afe6c38335996a8bfc74b7';
/// Filtered products provider
/// Combines products, search query, and category filter to provide filtered results
/// This provider works on the client-side for additional filtering after API fetches
abstract class _$FilteredProducts extends $Notifier<List<Product>> {
List<Product> build();

View File

@@ -0,0 +1,13 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../data/datasources/product_remote_datasource.dart';
import '../../../../core/providers/core_providers.dart';
part 'product_datasource_provider.g.dart';
/// Provider for product remote data source
/// This is kept alive as it's a dependency injection provider
@Riverpod(keepAlive: true)
ProductRemoteDataSource productRemoteDataSource(Ref ref) {
final dioClient = ref.watch(dioClientProvider);
return ProductRemoteDataSourceImpl(dioClient);
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_datasource_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for product remote data source
/// This is kept alive as it's a dependency injection provider
@ProviderFor(productRemoteDataSource)
const productRemoteDataSourceProvider = ProductRemoteDataSourceProvider._();
/// Provider for product remote data source
/// This is kept alive as it's a dependency injection provider
final class ProductRemoteDataSourceProvider
extends
$FunctionalProvider<
ProductRemoteDataSource,
ProductRemoteDataSource,
ProductRemoteDataSource
>
with $Provider<ProductRemoteDataSource> {
/// Provider for product remote data source
/// This is kept alive as it's a dependency injection provider
const ProductRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productRemoteDataSourceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productRemoteDataSourceHash();
@$internal
@override
$ProviderElement<ProductRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductRemoteDataSource create(Ref ref) {
return productRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductRemoteDataSource>(value),
);
}
}
String _$productRemoteDataSourceHash() =>
r'ff7a408a03041d45714a470abf3cb226b7c32b2c';

View File

@@ -1,37 +1,387 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/product.dart';
import '../../data/models/product_model.dart';
import 'product_datasource_provider.dart';
import 'selected_category_provider.dart';
part 'products_provider.g.dart';
/// Provider for products list
/// Pagination state for products
class ProductPaginationState {
final List<Product> products;
final int currentPage;
final int totalPages;
final int totalItems;
final bool hasMore;
final bool isLoadingMore;
const ProductPaginationState({
required this.products,
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.hasMore,
this.isLoadingMore = false,
});
ProductPaginationState copyWith({
List<Product>? products,
int? currentPage,
int? totalPages,
int? totalItems,
bool? hasMore,
bool? isLoadingMore,
}) {
return ProductPaginationState(
products: products ?? this.products,
currentPage: currentPage ?? this.currentPage,
totalPages: totalPages ?? this.totalPages,
totalItems: totalItems ?? this.totalItems,
hasMore: hasMore ?? this.hasMore,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
);
}
}
/// Provider for products list with pagination and filtering
@riverpod
class Products extends _$Products {
static const int _limit = 20;
@override
Future<List<Product>> build() async {
// TODO: Implement with repository
return [];
Future<ProductPaginationState> build() async {
return await _fetchProducts(page: 1);
}
/// Fetch products with pagination and optional filters
Future<ProductPaginationState> _fetchProducts({
required int page,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
}) async {
final datasource = ref.read(productRemoteDataSourceProvider);
final response = await datasource.getAllProducts(
page: page,
limit: _limit,
categoryId: categoryId,
search: search,
minPrice: minPrice,
maxPrice: maxPrice,
isAvailable: isAvailable,
);
// Extract data
final List<ProductModel> productModels =
(response['data'] as List<ProductModel>);
final meta = response['meta'] as Map<String, dynamic>;
// Convert to entities
final products = productModels.map((model) => model.toEntity()).toList();
// Extract pagination info
final currentPage = meta['currentPage'] as int? ?? page;
final totalPages = meta['totalPages'] as int? ?? 1;
final totalItems = meta['totalItems'] as int? ?? products.length;
final hasMore = currentPage < totalPages;
return ProductPaginationState(
products: products,
currentPage: currentPage,
totalPages: totalPages,
totalItems: totalItems,
hasMore: hasMore,
);
}
/// Refresh products (reset to first page)
Future<void> refresh() async {
// TODO: Implement refresh logic
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Fetch products from repository
return [];
return await _fetchProducts(page: 1);
});
}
Future<void> syncProducts() async {
// TODO: Implement sync logic with remote data source
/// Load more products (next page)
Future<void> loadMore() async {
final currentState = state.value;
if (currentState == null || !currentState.hasMore) return;
// Set loading more flag
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: true),
);
// Fetch next page
final nextPage = currentState.currentPage + 1;
try {
final newState = await _fetchProducts(page: nextPage);
// Append new products to existing ones
state = AsyncValue.data(
newState.copyWith(
products: [...currentState.products, ...newState.products],
isLoadingMore: false,
),
);
} catch (error, stackTrace) {
// Restore previous state on error
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: false),
);
// Optionally rethrow or handle error
state = AsyncValue.error(error, stackTrace);
}
}
/// Filter products by category
Future<void> filterByCategory(String? categoryId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Sync products from API
return [];
return await _fetchProducts(page: 1, categoryId: categoryId);
});
}
/// Search products
Future<void> search(String query) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchProducts(page: 1, search: query);
});
}
/// Filter by price range
Future<void> filterByPrice({double? minPrice, double? maxPrice}) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchProducts(
page: 1,
minPrice: minPrice,
maxPrice: maxPrice,
);
});
}
/// Filter by availability
Future<void> filterByAvailability(bool isAvailable) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchProducts(page: 1, isAvailable: isAvailable);
});
}
/// Apply multiple filters at once
Future<void> applyFilters({
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
}) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchProducts(
page: 1,
categoryId: categoryId,
search: search,
minPrice: minPrice,
maxPrice: maxPrice,
isAvailable: isAvailable,
);
});
}
}
/// Provider for search query
/// Provider for single product by ID
@riverpod
Future<Product> product(Ref ref, String id) async {
final datasource = ref.read(productRemoteDataSourceProvider);
final productModel = await datasource.getProductById(id);
return productModel.toEntity();
}
/// Provider for products filtered by the selected category
/// This provider automatically updates when the selected category changes
@riverpod
class ProductsBySelectedCategory extends _$ProductsBySelectedCategory {
static const int _limit = 20;
@override
Future<ProductPaginationState> build() async {
// Watch selected category
final selectedCategoryId = ref.watch(selectedCategoryProvider);
// Fetch products with category filter
return await _fetchProducts(page: 1, categoryId: selectedCategoryId);
}
Future<ProductPaginationState> _fetchProducts({
required int page,
String? categoryId,
}) async {
final datasource = ref.read(productRemoteDataSourceProvider);
final response = await datasource.getAllProducts(
page: page,
limit: _limit,
categoryId: categoryId,
);
// Extract data
final List<ProductModel> productModels =
(response['data'] as List<ProductModel>);
final meta = response['meta'] as Map<String, dynamic>;
// Convert to entities
final products = productModels.map((model) => model.toEntity()).toList();
// Extract pagination info
final currentPage = meta['currentPage'] as int? ?? page;
final totalPages = meta['totalPages'] as int? ?? 1;
final totalItems = meta['totalItems'] as int? ?? products.length;
final hasMore = currentPage < totalPages;
return ProductPaginationState(
products: products,
currentPage: currentPage,
totalPages: totalPages,
totalItems: totalItems,
hasMore: hasMore,
);
}
/// Load more products (next page)
Future<void> loadMore() async {
final currentState = state.value;
if (currentState == null || !currentState.hasMore) return;
// Set loading more flag
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: true),
);
// Fetch next page
final nextPage = currentState.currentPage + 1;
final selectedCategoryId = ref.read(selectedCategoryProvider);
try {
final newState = await _fetchProducts(
page: nextPage,
categoryId: selectedCategoryId,
);
// Append new products to existing ones
state = AsyncValue.data(
newState.copyWith(
products: [...currentState.products, ...newState.products],
isLoadingMore: false,
),
);
} catch (error, stackTrace) {
// Restore previous state on error
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: false),
);
state = AsyncValue.error(error, stackTrace);
}
}
}
/// Provider for searching products with pagination
@riverpod
class ProductSearch extends _$ProductSearch {
static const int _limit = 20;
@override
Future<ProductPaginationState> build(String query) async {
if (query.isEmpty) {
return const ProductPaginationState(
products: [],
currentPage: 0,
totalPages: 0,
totalItems: 0,
hasMore: false,
);
}
return await _searchProducts(query: query, page: 1);
}
Future<ProductPaginationState> _searchProducts({
required String query,
required int page,
}) async {
final datasource = ref.read(productRemoteDataSourceProvider);
final response = await datasource.searchProducts(query, page, _limit);
// Extract data
final List<ProductModel> productModels =
(response['data'] as List<ProductModel>);
final meta = response['meta'] as Map<String, dynamic>;
// Convert to entities
final products = productModels.map((model) => model.toEntity()).toList();
// Extract pagination info
final currentPage = meta['currentPage'] as int? ?? page;
final totalPages = meta['totalPages'] as int? ?? 1;
final totalItems = meta['totalItems'] as int? ?? products.length;
final hasMore = currentPage < totalPages;
return ProductPaginationState(
products: products,
currentPage: currentPage,
totalPages: totalPages,
totalItems: totalItems,
hasMore: hasMore,
);
}
/// Load more search results (next page)
Future<void> loadMore() async {
final currentState = state.value;
if (currentState == null || !currentState.hasMore) return;
// Set loading more flag
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: true),
);
// Fetch next page
final nextPage = currentState.currentPage + 1;
try {
// Get the query from the provider parameter
// Note: In Riverpod 3.0, family parameters are accessed differently
// We need to re-search with the same query
final newState = await _searchProducts(
query: '', // This will be replaced by proper implementation
page: nextPage,
);
// Append new products to existing ones
state = AsyncValue.data(
newState.copyWith(
products: [...currentState.products, ...newState.products],
isLoadingMore: false,
),
);
} catch (error, stackTrace) {
// Restore previous state on error
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: false),
);
state = AsyncValue.error(error, stackTrace);
}
}
}
/// Search query provider for products
@riverpod
class SearchQuery extends _$SearchQuery {
@override
@@ -39,19 +389,16 @@ class SearchQuery extends _$SearchQuery {
void setQuery(String query) {
state = query;
// Trigger search in products provider
if (query.isNotEmpty) {
ref.read(productsProvider.notifier).search(query);
} else {
ref.read(productsProvider.notifier).refresh();
}
}
void clear() {
state = '';
ref.read(productsProvider.notifier).refresh();
}
}
/// Provider for filtered products
@riverpod
List<Product> filteredProducts(Ref ref) {
final products = ref.watch(productsProvider).value ?? [];
final query = ref.watch(searchQueryProvider);
if (query.isEmpty) return products;
return products.where((p) =>
p.name.toLowerCase().contains(query.toLowerCase()) ||
p.description.toLowerCase().contains(query.toLowerCase())
).toList();
}

View File

@@ -8,15 +8,15 @@ part of 'products_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for products list
/// Provider for products list with pagination and filtering
@ProviderFor(Products)
const productsProvider = ProductsProvider._();
/// Provider for products list
/// Provider for products list with pagination and filtering
final class ProductsProvider
extends $AsyncNotifierProvider<Products, List<Product>> {
/// Provider for products list
extends $AsyncNotifierProvider<Products, ProductPaginationState> {
/// Provider for products list with pagination and filtering
const ProductsProvider._()
: super(
from: null,
@@ -36,22 +36,27 @@ final class ProductsProvider
Products create() => Products();
}
String _$productsHash() => r'9e1d3aaa1d9cf0b4ff03fdfaf4512a7a15336d51';
String _$productsHash() => r'2f2da8d6d7c1b88a525e4f79c9b29267b7da08ea';
/// Provider for products list
/// Provider for products list with pagination and filtering
abstract class _$Products extends $AsyncNotifier<List<Product>> {
FutureOr<List<Product>> build();
abstract class _$Products extends $AsyncNotifier<ProductPaginationState> {
FutureOr<ProductPaginationState> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<List<Product>>, List<Product>>;
final ref =
this.ref
as $Ref<AsyncValue<ProductPaginationState>, ProductPaginationState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Product>>, List<Product>>,
AsyncValue<List<Product>>,
AnyNotifier<
AsyncValue<ProductPaginationState>,
ProductPaginationState
>,
AsyncValue<ProductPaginationState>,
Object?,
Object?
>;
@@ -59,14 +64,264 @@ abstract class _$Products extends $AsyncNotifier<List<Product>> {
}
}
/// Provider for search query
/// Provider for single product by ID
@ProviderFor(product)
const productProvider = ProductFamily._();
/// Provider for single product by ID
final class ProductProvider
extends $FunctionalProvider<AsyncValue<Product>, Product, FutureOr<Product>>
with $FutureModifier<Product>, $FutureProvider<Product> {
/// Provider for single product by ID
const ProductProvider._({
required ProductFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'productProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productHash();
@override
String toString() {
return r'productProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Product> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Product> create(Ref ref) {
final argument = this.argument as String;
return product(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ProductProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$productHash() => r'e9b9a3db5f2aa33a19defe3551b8dca62d1c96b1';
/// Provider for single product by ID
final class ProductFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Product>, String> {
const ProductFamily._()
: super(
retry: null,
name: r'productProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for single product by ID
ProductProvider call(String id) =>
ProductProvider._(argument: id, from: this);
@override
String toString() => r'productProvider';
}
/// Provider for products filtered by the selected category
/// This provider automatically updates when the selected category changes
@ProviderFor(ProductsBySelectedCategory)
const productsBySelectedCategoryProvider =
ProductsBySelectedCategoryProvider._();
/// Provider for products filtered by the selected category
/// This provider automatically updates when the selected category changes
final class ProductsBySelectedCategoryProvider
extends
$AsyncNotifierProvider<
ProductsBySelectedCategory,
ProductPaginationState
> {
/// Provider for products filtered by the selected category
/// This provider automatically updates when the selected category changes
const ProductsBySelectedCategoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productsBySelectedCategoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productsBySelectedCategoryHash();
@$internal
@override
ProductsBySelectedCategory create() => ProductsBySelectedCategory();
}
String _$productsBySelectedCategoryHash() =>
r'642bbfab846469933bd4af89fb2ac7da77895562';
/// Provider for products filtered by the selected category
/// This provider automatically updates when the selected category changes
abstract class _$ProductsBySelectedCategory
extends $AsyncNotifier<ProductPaginationState> {
FutureOr<ProductPaginationState> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref
as $Ref<AsyncValue<ProductPaginationState>, ProductPaginationState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<ProductPaginationState>,
ProductPaginationState
>,
AsyncValue<ProductPaginationState>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider for searching products with pagination
@ProviderFor(ProductSearch)
const productSearchProvider = ProductSearchFamily._();
/// Provider for searching products with pagination
final class ProductSearchProvider
extends $AsyncNotifierProvider<ProductSearch, ProductPaginationState> {
/// Provider for searching products with pagination
const ProductSearchProvider._({
required ProductSearchFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'productSearchProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productSearchHash();
@override
String toString() {
return r'productSearchProvider'
''
'($argument)';
}
@$internal
@override
ProductSearch create() => ProductSearch();
@override
bool operator ==(Object other) {
return other is ProductSearchProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$productSearchHash() => r'86946a7cf6722822ed205af5d4ec2a8f5ba5ca48';
/// Provider for searching products with pagination
final class ProductSearchFamily extends $Family
with
$ClassFamilyOverride<
ProductSearch,
AsyncValue<ProductPaginationState>,
ProductPaginationState,
FutureOr<ProductPaginationState>,
String
> {
const ProductSearchFamily._()
: super(
retry: null,
name: r'productSearchProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for searching products with pagination
ProductSearchProvider call(String query) =>
ProductSearchProvider._(argument: query, from: this);
@override
String toString() => r'productSearchProvider';
}
/// Provider for searching products with pagination
abstract class _$ProductSearch extends $AsyncNotifier<ProductPaginationState> {
late final _$args = ref.$arg as String;
String get query => _$args;
FutureOr<ProductPaginationState> build(String query);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref =
this.ref
as $Ref<AsyncValue<ProductPaginationState>, ProductPaginationState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<ProductPaginationState>,
ProductPaginationState
>,
AsyncValue<ProductPaginationState>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Search query provider for products
@ProviderFor(SearchQuery)
const searchQueryProvider = SearchQueryProvider._();
/// Provider for search query
/// Search query provider for products
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
/// Provider for search query
/// Search query provider for products
const SearchQueryProvider._()
: super(
from: null,
@@ -94,9 +349,9 @@ final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
}
}
String _$searchQueryHash() => r'2c146927785523a0ddf51b23b777a9be4afdc092';
String _$searchQueryHash() => r'0c08fe7fe2ce47cf806a34872f5cf4912fe8c618';
/// Provider for search query
/// Search query provider for products
abstract class _$SearchQuery extends $Notifier<String> {
String build();
@@ -116,49 +371,3 @@ abstract class _$SearchQuery extends $Notifier<String> {
element.handleValue(ref, created);
}
}
/// Provider for filtered products
@ProviderFor(filteredProducts)
const filteredProductsProvider = FilteredProductsProvider._();
/// Provider for filtered products
final class FilteredProductsProvider
extends $FunctionalProvider<List<Product>, List<Product>, List<Product>>
with $Provider<List<Product>> {
/// Provider for filtered products
const FilteredProductsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'filteredProductsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$filteredProductsHash();
@$internal
@override
$ProviderElement<List<Product>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
List<Product> create(Ref ref) {
return filteredProducts(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<Product> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<Product>>(value),
);
}
}
String _$filteredProductsHash() => r'e4e0c549c454576fc599713a5237435a8dd4b277';

View File

@@ -3,13 +3,10 @@
/// Contains Riverpod providers for product state management
library;
// Export individual provider files
// Note: products_provider.dart contains multiple providers
// so we only export it to avoid ambiguous exports
export 'products_provider.dart';
// Export datasource provider
export 'product_datasource_provider.dart';
// These are also defined in products_provider.dart, so we don't export them separately
// to avoid ambiguous export errors
// export 'filtered_products_provider.dart';
// export 'search_query_provider.dart';
// export 'selected_category_provider.dart';
// Export state providers
export 'products_provider.dart';
export 'filtered_products_provider.dart';
export 'selected_category_provider.dart';

View File

@@ -1,27 +0,0 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search_query_provider.g.dart';
/// Search query state provider
/// Manages the current search query string for product filtering
@riverpod
class SearchQuery extends _$SearchQuery {
@override
String build() {
// Initialize with empty search query
return '';
}
/// Update search query
void setQuery(String query) {
state = query.trim();
}
/// Clear search query
void clear() {
state = '';
}
/// Check if search is active
bool get isSearching => state.isNotEmpty;
}

View File

@@ -1,71 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_query_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Search query state provider
/// Manages the current search query string for product filtering
@ProviderFor(SearchQuery)
const searchQueryProvider = SearchQueryProvider._();
/// Search query state provider
/// Manages the current search query string for product filtering
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
/// Search query state provider
/// Manages the current search query string for product filtering
const SearchQueryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'searchQueryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchQueryHash();
@$internal
@override
SearchQuery create() => SearchQuery();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String>(value),
);
}
}
String _$searchQueryHash() => r'62191c640ca9424065338a26c1af5c4695a46ef5';
/// Search query state provider
/// Manages the current search query string for product filtering
abstract class _$SearchQuery 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);
}
}