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

@@ -28,7 +28,7 @@ class CategoriesPage extends ConsumerWidget {
),
body: RefreshIndicator(
onRefresh: () async {
await ref.refresh(categoriesProvider.future);
ref.read(categoriesProvider.notifier).refresh();
},
child: categoriesAsync.when(
loading: () => const Center(

View File

@@ -1,5 +1,9 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/category.dart';
import '../../data/models/category_model.dart';
import '../../../products/data/models/product_model.dart';
import '../../../products/domain/entities/product.dart';
import 'category_remote_datasource_provider.dart';
part 'categories_provider.g.dart';
@@ -8,33 +12,182 @@ part 'categories_provider.g.dart';
class Categories extends _$Categories {
@override
Future<List<Category>> build() async {
// TODO: Implement with repository
return [];
return await _fetchCategories();
}
Future<List<Category>> _fetchCategories() async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final categoryModels = await datasource.getAllCategories();
return categoryModels.map((model) => model.toEntity()).toList();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Fetch categories from repository
return [];
});
}
Future<void> syncCategories() async {
// TODO: Implement sync logic with remote data source
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Sync categories from API
return [];
return await _fetchCategories();
});
}
}
/// Provider for selected category
/// Provider for single category by ID
@riverpod
class SelectedCategory extends _$SelectedCategory {
Future<Category> category(Ref ref, String id) async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final categoryModel = await datasource.getCategoryById(id);
return categoryModel.toEntity();
}
/// Pagination state for category products
class CategoryProductsState {
final Category category;
final List<Product> products;
final int currentPage;
final int totalPages;
final int totalItems;
final bool hasMore;
final bool isLoadingMore;
const CategoryProductsState({
required this.category,
required this.products,
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.hasMore,
this.isLoadingMore = false,
});
CategoryProductsState copyWith({
Category? category,
List<Product>? products,
int? currentPage,
int? totalPages,
int? totalItems,
bool? hasMore,
bool? isLoadingMore,
}) {
return CategoryProductsState(
category: category ?? this.category,
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 category with its products (with pagination)
@riverpod
class CategoryWithProducts extends _$CategoryWithProducts {
static const int _limit = 20;
@override
String? build() => null;
Future<CategoryProductsState> build(String categoryId) async {
return await _fetchCategoryWithProducts(categoryId: categoryId, page: 1);
}
Future<CategoryProductsState> _fetchCategoryWithProducts({
required String categoryId,
required int page,
}) async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final response = await datasource.getCategoryWithProducts(
categoryId,
page,
_limit,
);
// Extract data
final CategoryModel categoryModel = response['category'] as CategoryModel;
final List<dynamic> productsJson = response['products'] as List<dynamic>;
final meta = response['meta'] as Map<String, dynamic>;
// Convert category to entity
final category = categoryModel.toEntity();
// Convert products to entities
final products = productsJson
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.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 CategoryProductsState(
category: category,
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;
try {
final newState = await _fetchCategoryWithProducts(
categoryId: currentState.category.id,
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);
}
}
/// Refresh category and products
Future<void> refresh() async {
final currentState = state.value;
if (currentState == null) return;
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchCategoryWithProducts(
categoryId: currentState.category.id,
page: 1,
);
});
}
}
/// Provider for selected category state
/// This is used in the products feature for filtering
@riverpod
class SelectedCategoryInCategories extends _$SelectedCategoryInCategories {
@override
String? build() {
return null;
}
void select(String? categoryId) {
state = categoryId;
@@ -43,4 +196,8 @@ class SelectedCategory extends _$SelectedCategory {
void clear() {
state = null;
}
bool get hasSelection => state != null;
bool isSelected(String categoryId) => state == categoryId;
}

View File

@@ -36,7 +36,7 @@ final class CategoriesProvider
Categories create() => Categories();
}
String _$categoriesHash() => r'aa7afc38a5567b0f42ff05ca23b287baa4780cbe';
String _$categoriesHash() => r'5156d31a6d7b9457c4735b66e170b262140758e2';
/// Provider for categories list
@@ -59,32 +59,223 @@ abstract class _$Categories extends $AsyncNotifier<List<Category>> {
}
}
/// Provider for selected category
/// Provider for single category by ID
@ProviderFor(SelectedCategory)
const selectedCategoryProvider = SelectedCategoryProvider._();
@ProviderFor(category)
const categoryProvider = CategoryFamily._();
/// Provider for selected category
final class SelectedCategoryProvider
extends $NotifierProvider<SelectedCategory, String?> {
/// Provider for selected category
const SelectedCategoryProvider._()
/// Provider for single category by ID
final class CategoryProvider
extends
$FunctionalProvider<AsyncValue<Category>, Category, FutureOr<Category>>
with $FutureModifier<Category>, $FutureProvider<Category> {
/// Provider for single category by ID
const CategoryProvider._({
required CategoryFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'categoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$categoryHash();
@override
String toString() {
return r'categoryProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Category> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Category> create(Ref ref) {
final argument = this.argument as String;
return category(ref, argument);
}
@override
bool operator ==(Object other) {
return other is CategoryProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$categoryHash() => r'e26dd362e42a1217a774072f453a64c7a6195e73';
/// Provider for single category by ID
final class CategoryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Category>, String> {
const CategoryFamily._()
: super(
retry: null,
name: r'categoryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for single category by ID
CategoryProvider call(String id) =>
CategoryProvider._(argument: id, from: this);
@override
String toString() => r'categoryProvider';
}
/// Provider for category with its products (with pagination)
@ProviderFor(CategoryWithProducts)
const categoryWithProductsProvider = CategoryWithProductsFamily._();
/// Provider for category with its products (with pagination)
final class CategoryWithProductsProvider
extends
$AsyncNotifierProvider<CategoryWithProducts, CategoryProductsState> {
/// Provider for category with its products (with pagination)
const CategoryWithProductsProvider._({
required CategoryWithProductsFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'categoryWithProductsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$categoryWithProductsHash();
@override
String toString() {
return r'categoryWithProductsProvider'
''
'($argument)';
}
@$internal
@override
CategoryWithProducts create() => CategoryWithProducts();
@override
bool operator ==(Object other) {
return other is CategoryWithProductsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$categoryWithProductsHash() =>
r'a5ea35fad4e711ea855e4874f9135145d7d44b67';
/// Provider for category with its products (with pagination)
final class CategoryWithProductsFamily extends $Family
with
$ClassFamilyOverride<
CategoryWithProducts,
AsyncValue<CategoryProductsState>,
CategoryProductsState,
FutureOr<CategoryProductsState>,
String
> {
const CategoryWithProductsFamily._()
: super(
retry: null,
name: r'categoryWithProductsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for category with its products (with pagination)
CategoryWithProductsProvider call(String categoryId) =>
CategoryWithProductsProvider._(argument: categoryId, from: this);
@override
String toString() => r'categoryWithProductsProvider';
}
/// Provider for category with its products (with pagination)
abstract class _$CategoryWithProducts
extends $AsyncNotifier<CategoryProductsState> {
late final _$args = ref.$arg as String;
String get categoryId => _$args;
FutureOr<CategoryProductsState> build(String categoryId);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref =
this.ref
as $Ref<AsyncValue<CategoryProductsState>, CategoryProductsState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<CategoryProductsState>,
CategoryProductsState
>,
AsyncValue<CategoryProductsState>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider for selected category state
/// This is used in the products feature for filtering
@ProviderFor(SelectedCategoryInCategories)
const selectedCategoryInCategoriesProvider =
SelectedCategoryInCategoriesProvider._();
/// Provider for selected category state
/// This is used in the products feature for filtering
final class SelectedCategoryInCategoriesProvider
extends $NotifierProvider<SelectedCategoryInCategories, String?> {
/// Provider for selected category state
/// This is used in the products feature for filtering
const SelectedCategoryInCategoriesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedCategoryProvider',
name: r'selectedCategoryInCategoriesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedCategoryHash();
String debugGetCreateSourceHash() => _$selectedCategoryInCategoriesHash();
@$internal
@override
SelectedCategory create() => SelectedCategory();
SelectedCategoryInCategories create() => SelectedCategoryInCategories();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
@@ -95,11 +286,13 @@ final class SelectedCategoryProvider
}
}
String _$selectedCategoryHash() => r'a47cd2de07ad285d4b73b2294ba954cb1cdd8e4c';
String _$selectedCategoryInCategoriesHash() =>
r'510d79a73dcfeba5efa886f5f95f7470dbd09a47';
/// Provider for selected category
/// Provider for selected category state
/// This is used in the products feature for filtering
abstract class _$SelectedCategory extends $Notifier<String?> {
abstract class _$SelectedCategoryInCategories extends $Notifier<String?> {
String? build();
@$mustCallSuper
@override

View File

@@ -1,14 +0,0 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../data/datasources/category_local_datasource.dart';
import '../../../../core/database/hive_database.dart';
import '../../data/models/category_model.dart';
part 'category_datasource_provider.g.dart';
/// Provider for category local data source
/// This is kept alive as it's a dependency injection provider
@Riverpod(keepAlive: true)
CategoryLocalDataSource categoryLocalDataSource(Ref ref) {
final box = HiveDatabase.instance.getBox<CategoryModel>('categories');
return CategoryLocalDataSourceImpl(box);
}

View File

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

View File

@@ -1,35 +0,0 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../products/presentation/providers/products_provider.dart';
part 'category_product_count_provider.g.dart';
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
@riverpod
int categoryProductCount(Ref ref, String categoryId) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
data: (products) => products.where((p) => p.categoryId == categoryId).length,
loading: () => 0,
error: (_, __) => 0,
);
}
/// Provider that returns all category product counts as a map
/// Useful for displaying product counts on all category cards at once
@riverpod
Map<String, int> allCategoryProductCounts(Ref ref) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
data: (products) {
// Group products by category and count
final counts = <String, int>{};
for (final product in products) {
counts[product.categoryId] = (counts[product.categoryId] ?? 0) + 1;
}
return counts;
},
loading: () => {},
error: (_, __) => {},
);
}

View File

@@ -1,156 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'category_product_count_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
@ProviderFor(categoryProductCount)
const categoryProductCountProvider = CategoryProductCountFamily._();
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
final class CategoryProductCountProvider
extends $FunctionalProvider<int, int, int>
with $Provider<int> {
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
const CategoryProductCountProvider._({
required CategoryProductCountFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'categoryProductCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$categoryProductCountHash();
@override
String toString() {
return r'categoryProductCountProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
int create(Ref ref) {
final argument = this.argument as String;
return categoryProductCount(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
@override
bool operator ==(Object other) {
return other is CategoryProductCountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$categoryProductCountHash() =>
r'2d51eea21a4d018964d10ee00d0957a2c38d28c6';
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
final class CategoryProductCountFamily extends $Family
with $FunctionalFamilyOverride<int, String> {
const CategoryProductCountFamily._()
: super(
retry: null,
name: r'categoryProductCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider that calculates product count for a specific category
/// Uses family pattern to create a provider for each category ID
CategoryProductCountProvider call(String categoryId) =>
CategoryProductCountProvider._(argument: categoryId, from: this);
@override
String toString() => r'categoryProductCountProvider';
}
/// Provider that returns all category product counts as a map
/// Useful for displaying product counts on all category cards at once
@ProviderFor(allCategoryProductCounts)
const allCategoryProductCountsProvider = AllCategoryProductCountsProvider._();
/// Provider that returns all category product counts as a map
/// Useful for displaying product counts on all category cards at once
final class AllCategoryProductCountsProvider
extends
$FunctionalProvider<
Map<String, int>,
Map<String, int>,
Map<String, int>
>
with $Provider<Map<String, int>> {
/// Provider that returns all category product counts as a map
/// Useful for displaying product counts on all category cards at once
const AllCategoryProductCountsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'allCategoryProductCountsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$allCategoryProductCountsHash();
@$internal
@override
$ProviderElement<Map<String, int>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Map<String, int> create(Ref ref) {
return allCategoryProductCounts(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, int> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, int>>(value),
);
}
}
String _$allCategoryProductCountsHash() =>
r'a4ecc281916772ac74327333bd76e7b6463a0992';

View File

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

View File

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

View File

@@ -3,11 +3,8 @@
/// Contains Riverpod providers for category state management
library;
export 'category_datasource_provider.dart';
export 'categories_provider.dart';
export 'category_product_count_provider.dart';
// Export datasource providers
export 'category_remote_datasource_provider.dart';
// Note: SelectedCategory provider is defined in categories_provider.dart
// but we avoid exporting it separately to prevent ambiguous exports with
// the products feature. Use selectedCategoryProvider directly from
// categories_provider.dart or from products feature.
// Export state providers
export 'categories_provider.dart';