runable
This commit is contained in:
817
.claude/agents/riverpod-expert-non-codegen.md
Normal file
817
.claude/agents/riverpod-expert-non-codegen.md
Normal file
@@ -0,0 +1,817 @@
|
||||
---
|
||||
name: riverpod-non-code-gen-expert
|
||||
description: Riverpod state management specialist. MUST BE USED for all state management, providers, and reactive programming tasks. Focuses on manual provider creation without code generation.
|
||||
tools: Read, Write, Edit, Grep, Bash
|
||||
---
|
||||
|
||||
You are a Riverpod 3.0 expert specializing in:
|
||||
- Manual provider creation and organization
|
||||
- State management with Notifier, AsyncNotifier, and StreamNotifier
|
||||
- Implementing proper state management patterns
|
||||
- Handling async operations and loading states
|
||||
- Testing providers and state logic
|
||||
- Provider composition and dependencies
|
||||
|
||||
## Key Philosophy:
|
||||
**This guide focuses on manual provider creation WITHOUT code generation.** While code generation is available, this approach gives you full control and doesn't require build_runner setup.
|
||||
|
||||
## Modern Provider Types (Manual Creation):
|
||||
|
||||
### Basic Providers:
|
||||
|
||||
#### Provider - Immutable Values & Dependencies
|
||||
For values that never change or dependency injection:
|
||||
|
||||
```dart
|
||||
// Simple value
|
||||
final appNameProvider = Provider<String>((ref) => 'Retail POS');
|
||||
|
||||
// Configuration
|
||||
final apiBaseUrlProvider = Provider<String>((ref) {
|
||||
return const String.fromEnvironment('API_URL',
|
||||
defaultValue: 'http://localhost:3000');
|
||||
});
|
||||
|
||||
// Dependency injection
|
||||
final dioProvider = Provider<Dio>((ref) {
|
||||
final dio = Dio(BaseOptions(
|
||||
baseUrl: ref.watch(apiBaseUrlProvider),
|
||||
));
|
||||
return dio;
|
||||
});
|
||||
|
||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||||
return ApiClient(ref.watch(dioProvider));
|
||||
});
|
||||
```
|
||||
|
||||
#### FutureProvider - One-Time Async Operations
|
||||
For async data that loads once:
|
||||
|
||||
```dart
|
||||
// Fetch user profile
|
||||
final userProfileProvider = FutureProvider<User>((ref) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getUser();
|
||||
});
|
||||
|
||||
// With parameters (Family)
|
||||
final postProvider = FutureProvider.family<Post, String>((ref, postId) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getPost(postId);
|
||||
});
|
||||
|
||||
// Auto dispose when not used
|
||||
final productProvider = FutureProvider.autoDispose.family<Product, String>(
|
||||
(ref, productId) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getProduct(productId);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
#### StreamProvider - Continuous Data Streams
|
||||
For streaming data (WebSocket, Firestore, etc.):
|
||||
|
||||
```dart
|
||||
// WebSocket messages
|
||||
final messagesStreamProvider = StreamProvider<Message>((ref) {
|
||||
final webSocket = ref.watch(webSocketProvider);
|
||||
return webSocket.messages;
|
||||
});
|
||||
|
||||
// Firestore real-time updates
|
||||
final notificationsProvider = StreamProvider.autoDispose<List<Notification>>(
|
||||
(ref) {
|
||||
final firestore = ref.watch(firestoreProvider);
|
||||
return firestore.collection('notifications').snapshots().map(
|
||||
(snapshot) => snapshot.docs.map((doc) => Notification.fromDoc(doc)).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Modern Mutable State Providers:
|
||||
|
||||
#### NotifierProvider - Synchronous Mutable State
|
||||
For complex state with methods (replaces StateNotifierProvider):
|
||||
|
||||
```dart
|
||||
// Counter with methods
|
||||
class Counter extends Notifier<int> {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void increment() => state++;
|
||||
void decrement() => state--;
|
||||
void reset() => state = 0;
|
||||
void setValue(int value) => state = value;
|
||||
}
|
||||
|
||||
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
|
||||
|
||||
// With auto dispose
|
||||
final counterProvider = NotifierProvider.autoDispose<Counter, int>(Counter.new);
|
||||
|
||||
// Cart management
|
||||
class Cart extends Notifier<List<CartItem>> {
|
||||
@override
|
||||
List<CartItem> build() => [];
|
||||
|
||||
void addItem(Product product, int quantity) {
|
||||
state = [
|
||||
...state,
|
||||
CartItem(
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
price: product.price,
|
||||
quantity: quantity,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void removeItem(String productId) {
|
||||
state = state.where((item) => item.productId != productId).toList();
|
||||
}
|
||||
|
||||
void updateQuantity(String productId, int quantity) {
|
||||
state = state.map((item) {
|
||||
if (item.productId == productId) {
|
||||
return item.copyWith(quantity: quantity);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void clear() => state = [];
|
||||
}
|
||||
|
||||
final cartProvider = NotifierProvider<Cart, List<CartItem>>(Cart.new);
|
||||
```
|
||||
|
||||
#### AsyncNotifierProvider - Async Mutable State
|
||||
For state that requires async initialization and mutations:
|
||||
|
||||
```dart
|
||||
// User profile with async loading
|
||||
class UserProfile extends AsyncNotifier<User> {
|
||||
@override
|
||||
Future<User> build() async {
|
||||
// Async initialization
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getCurrentUser();
|
||||
}
|
||||
|
||||
Future<void> updateName(String name) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.updateUserName(name);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getCurrentUser();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final userProfileProvider = AsyncNotifierProvider<UserProfile, User>(
|
||||
UserProfile.new,
|
||||
);
|
||||
|
||||
// With auto dispose
|
||||
final userProfileProvider = AsyncNotifierProvider.autoDispose<UserProfile, User>(
|
||||
UserProfile.new,
|
||||
);
|
||||
|
||||
// Products list with filtering
|
||||
class ProductsList extends AsyncNotifier<List<Product>> {
|
||||
@override
|
||||
Future<List<Product>> build() async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getProducts();
|
||||
}
|
||||
|
||||
Future<void> syncProducts() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getProducts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final productsProvider = AsyncNotifierProvider<ProductsList, List<Product>>(
|
||||
ProductsList.new,
|
||||
);
|
||||
```
|
||||
|
||||
#### StreamNotifierProvider - Stream-based Mutable State
|
||||
For streaming data with methods:
|
||||
|
||||
```dart
|
||||
class ChatMessages extends StreamNotifier<List<Message>> {
|
||||
@override
|
||||
Stream<List<Message>> build() {
|
||||
final chatService = ref.watch(chatServiceProvider);
|
||||
return chatService.messagesStream();
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String text) async {
|
||||
final chatService = ref.watch(chatServiceProvider);
|
||||
await chatService.send(text);
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
final chatService = ref.watch(chatServiceProvider);
|
||||
await chatService.delete(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
final chatMessagesProvider = StreamNotifierProvider<ChatMessages, List<Message>>(
|
||||
ChatMessages.new,
|
||||
);
|
||||
```
|
||||
|
||||
### Legacy Providers (Discouraged):
|
||||
|
||||
❌ **Don't use these in new code:**
|
||||
- `StateProvider` → Use `NotifierProvider` instead
|
||||
- `StateNotifierProvider` → Use `NotifierProvider` instead
|
||||
- `ChangeNotifierProvider` → Use `NotifierProvider` instead
|
||||
|
||||
## Family Modifier - Parameters:
|
||||
|
||||
```dart
|
||||
// FutureProvider with family
|
||||
final productProvider = FutureProvider.family<Product, String>(
|
||||
(ref, productId) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getProduct(productId);
|
||||
},
|
||||
);
|
||||
|
||||
// NotifierProvider with family
|
||||
class ProductDetails extends FamilyNotifier<Product, String> {
|
||||
@override
|
||||
Product build(String productId) {
|
||||
// Load product by ID
|
||||
final products = ref.watch(productsProvider).value ?? [];
|
||||
return products.firstWhere((p) => p.id == productId);
|
||||
}
|
||||
|
||||
void updateStock(int quantity) {
|
||||
state = state.copyWith(stockQuantity: quantity);
|
||||
}
|
||||
}
|
||||
|
||||
final productDetailsProvider = NotifierProvider.family<ProductDetails, Product, String>(
|
||||
ProductDetails.new,
|
||||
);
|
||||
|
||||
// AsyncNotifierProvider with family
|
||||
class PostDetail extends FamilyAsyncNotifier<Post, String> {
|
||||
@override
|
||||
Future<Post> build(String postId) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.getPost(postId);
|
||||
}
|
||||
|
||||
Future<void> like() async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
await api.likePost(arg);
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
final postDetailProvider = AsyncNotifierProvider.family<PostDetail, Post, String>(
|
||||
PostDetail.new,
|
||||
);
|
||||
```
|
||||
|
||||
## Always Check First:
|
||||
- `pubspec.yaml` - Ensure riverpod packages are installed
|
||||
- Existing provider patterns and organization
|
||||
- Current Riverpod version (target 3.0+)
|
||||
|
||||
## Setup Requirements:
|
||||
|
||||
### pubspec.yaml:
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter_riverpod: ^3.0.0
|
||||
# No code generation packages needed
|
||||
|
||||
dev_dependencies:
|
||||
riverpod_lint: ^3.0.0
|
||||
custom_lint: ^0.6.0
|
||||
```
|
||||
|
||||
### Enable riverpod_lint:
|
||||
Create `analysis_options.yaml`:
|
||||
```yaml
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
```
|
||||
|
||||
## Provider Organization:
|
||||
|
||||
```
|
||||
lib/
|
||||
features/
|
||||
auth/
|
||||
providers/
|
||||
auth_provider.dart # Auth state
|
||||
auth_repository_provider.dart # Repository DI
|
||||
models/
|
||||
...
|
||||
products/
|
||||
providers/
|
||||
products_provider.dart
|
||||
product_search_provider.dart
|
||||
...
|
||||
```
|
||||
|
||||
## Key Patterns:
|
||||
|
||||
### 1. Dependency Injection:
|
||||
```dart
|
||||
// Provide dependencies
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
return AuthRepositoryImpl(
|
||||
api: ref.watch(apiClientProvider),
|
||||
storage: ref.watch(secureStorageProvider),
|
||||
);
|
||||
});
|
||||
|
||||
// Use in other providers
|
||||
final authProvider = AsyncNotifierProvider<Auth, User?>(Auth.new);
|
||||
|
||||
class Auth extends AsyncNotifier<User?> {
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
final repo = ref.read(authRepositoryProvider);
|
||||
return await repo.getCurrentUser();
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repo = ref.read(authRepositoryProvider);
|
||||
return await repo.login(email, password);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final repo = ref.read(authRepositoryProvider);
|
||||
await repo.logout();
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Provider Composition:
|
||||
```dart
|
||||
// Depend on other providers
|
||||
final filteredProductsProvider = Provider<List<Product>>((ref) {
|
||||
final products = ref.watch(productsProvider).value ?? [];
|
||||
final searchQuery = ref.watch(searchQueryProvider);
|
||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
|
||||
return products.where((product) {
|
||||
final matchesSearch = product.name
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.toLowerCase());
|
||||
final matchesCategory = selectedCategory == null ||
|
||||
product.categoryId == selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
});
|
||||
|
||||
// Computed values
|
||||
final cartTotalProvider = Provider<double>((ref) {
|
||||
final items = ref.watch(cartProvider);
|
||||
return items.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
||||
});
|
||||
|
||||
// Combine multiple providers
|
||||
final dashboardProvider = FutureProvider<Dashboard>((ref) async {
|
||||
final user = await ref.watch(userProfileProvider.future);
|
||||
final products = await ref.watch(productsProvider.future);
|
||||
final stats = await ref.watch(statsProvider.future);
|
||||
|
||||
return Dashboard(user: user, products: products, stats: stats);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Loading States:
|
||||
```dart
|
||||
// In widgets - using .when()
|
||||
ref.watch(userProfileProvider).when(
|
||||
data: (user) => UserView(user),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
error: (error, stack) => ErrorView(error),
|
||||
);
|
||||
|
||||
// Or pattern matching (Dart 3.0+)
|
||||
final userState = ref.watch(userProfileProvider);
|
||||
switch (userState) {
|
||||
case AsyncData(:final value):
|
||||
return UserView(value);
|
||||
case AsyncError(:final error):
|
||||
return ErrorView(error);
|
||||
case AsyncLoading():
|
||||
return CircularProgressIndicator();
|
||||
}
|
||||
|
||||
// Check states directly
|
||||
if (userState.isLoading) return LoadingWidget();
|
||||
if (userState.hasError) return ErrorWidget(userState.error);
|
||||
final user = userState.value!;
|
||||
```
|
||||
|
||||
### 4. Selective Watching (Performance):
|
||||
```dart
|
||||
// Bad - rebuilds on any user change
|
||||
final user = ref.watch(userProfileProvider);
|
||||
|
||||
// Good - rebuilds only when name changes
|
||||
final name = ref.watch(
|
||||
userProfileProvider.select((user) => user.value?.name)
|
||||
);
|
||||
|
||||
// In providers
|
||||
final userNameProvider = Provider<String?>((ref) {
|
||||
return ref.watch(
|
||||
userProfileProvider.select((async) => async.value?.name)
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Invalidation and Refresh:
|
||||
```dart
|
||||
// Invalidate provider (triggers rebuild)
|
||||
ref.invalidate(userProfileProvider);
|
||||
|
||||
// Refresh (invalidate and re-read immediately)
|
||||
ref.refresh(userProfileProvider);
|
||||
|
||||
// Invalidate from within Notifier
|
||||
class Products extends AsyncNotifier<List<Product>> {
|
||||
@override
|
||||
Future<List<Product>> build() async {
|
||||
return await _fetch();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<List<Product>> _fetch() async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return await api.getProducts();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. AutoDispose:
|
||||
```dart
|
||||
// Auto dispose when no longer used
|
||||
final dataProvider = FutureProvider.autoDispose<Data>((ref) async {
|
||||
return await fetchData();
|
||||
});
|
||||
|
||||
// Keep alive conditionally
|
||||
final dataProvider = FutureProvider.autoDispose<Data>((ref) async {
|
||||
final link = ref.keepAlive();
|
||||
|
||||
// Keep alive for 5 minutes after last listener
|
||||
Timer(const Duration(minutes: 5), link.close);
|
||||
|
||||
return await fetchData();
|
||||
});
|
||||
|
||||
// Check if still mounted after async operations
|
||||
class TodoList extends AutoDisposeNotifier<List<Todo>> {
|
||||
@override
|
||||
List<Todo> build() => [];
|
||||
|
||||
Future<void> addTodo(Todo todo) async {
|
||||
await api.saveTodo(todo);
|
||||
|
||||
// Check if still mounted
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = [...state, todo];
|
||||
}
|
||||
}
|
||||
|
||||
final todoListProvider = NotifierProvider.autoDispose<TodoList, List<Todo>>(
|
||||
TodoList.new,
|
||||
);
|
||||
```
|
||||
|
||||
## Consumer Widgets:
|
||||
|
||||
### ConsumerWidget:
|
||||
```dart
|
||||
class MyWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(counterProvider);
|
||||
return Text('$count');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ConsumerStatefulWidget:
|
||||
```dart
|
||||
class MyWidget extends ConsumerStatefulWidget {
|
||||
@override
|
||||
ConsumerState<MyWidget> createState() => _MyWidgetState();
|
||||
}
|
||||
|
||||
class _MyWidgetState extends ConsumerState<MyWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// ref is available in all lifecycle methods
|
||||
ref.read(counterProvider.notifier).increment();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final count = ref.watch(counterProvider);
|
||||
return Text('$count');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Consumer (for optimization):
|
||||
```dart
|
||||
Column(
|
||||
children: [
|
||||
const Text('Static content'),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final count = ref.watch(counterProvider);
|
||||
return Text('$count');
|
||||
},
|
||||
),
|
||||
const Text('More static content'),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Testing:
|
||||
|
||||
```dart
|
||||
test('counter increments', () {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
expect(container.read(counterProvider), 0);
|
||||
container.read(counterProvider.notifier).increment();
|
||||
expect(container.read(counterProvider), 1);
|
||||
});
|
||||
|
||||
// Async provider testing
|
||||
test('fetches user', () async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
authRepositoryProvider.overrideWithValue(MockAuthRepository()),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final user = await container.read(userProfileProvider.future);
|
||||
expect(user.name, 'Test User');
|
||||
});
|
||||
|
||||
// Widget testing
|
||||
testWidgets('displays user name', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
userProfileProvider.overrideWith((ref) =>
|
||||
const AsyncValue.data(User(name: 'Test'))
|
||||
),
|
||||
],
|
||||
child: MaterialApp(home: UserScreen()),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Common Patterns:
|
||||
|
||||
### Pagination:
|
||||
```dart
|
||||
class PostList extends Notifier<List<Post>> {
|
||||
@override
|
||||
List<Post> build() {
|
||||
_fetchPage(0);
|
||||
return [];
|
||||
}
|
||||
|
||||
int _page = 0;
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_page++;
|
||||
|
||||
try {
|
||||
final newPosts = await _fetchPage(_page);
|
||||
state = [...state, ...newPosts];
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Post>> _fetchPage(int page) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return await api.getPosts(page: page);
|
||||
}
|
||||
}
|
||||
|
||||
final postListProvider = NotifierProvider<PostList, List<Post>>(
|
||||
PostList.new,
|
||||
);
|
||||
```
|
||||
|
||||
### Form State:
|
||||
```dart
|
||||
class LoginForm extends Notifier<LoginFormState> {
|
||||
@override
|
||||
LoginFormState build() => LoginFormState();
|
||||
|
||||
void setEmail(String email) {
|
||||
state = state.copyWith(email: email);
|
||||
}
|
||||
|
||||
void setPassword(String password) {
|
||||
state = state.copyWith(password: password);
|
||||
}
|
||||
|
||||
Future<void> submit() async {
|
||||
if (!state.isValid) return;
|
||||
|
||||
state = state.copyWith(isLoading: true);
|
||||
|
||||
try {
|
||||
final repo = ref.read(authRepositoryProvider);
|
||||
await repo.login(state.email, state.password);
|
||||
state = state.copyWith(isLoading: false, isSuccess: true);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final loginFormProvider = NotifierProvider<LoginForm, LoginFormState>(
|
||||
LoginForm.new,
|
||||
);
|
||||
```
|
||||
|
||||
### Search with Debounce:
|
||||
```dart
|
||||
final searchQueryProvider = StateProvider<String>((ref) => '');
|
||||
|
||||
final debouncedSearchProvider = Provider<String>((ref) {
|
||||
final query = ref.watch(searchQueryProvider);
|
||||
|
||||
// Debounce logic
|
||||
final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
|
||||
debouncer.run(() {
|
||||
// Perform search
|
||||
});
|
||||
|
||||
return query;
|
||||
});
|
||||
|
||||
final searchResultsProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
|
||||
final query = ref.watch(debouncedSearchProvider);
|
||||
|
||||
if (query.isEmpty) return [];
|
||||
|
||||
final api = ref.watch(apiClientProvider);
|
||||
return await api.searchProducts(query);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices:
|
||||
|
||||
### Naming Conventions:
|
||||
```dart
|
||||
// Providers end with 'Provider'
|
||||
final userProvider = ...;
|
||||
final productsProvider = ...;
|
||||
|
||||
// Notifier classes are descriptive
|
||||
class Counter extends Notifier<int> { ... }
|
||||
class UserProfile extends AsyncNotifier<User> { ... }
|
||||
```
|
||||
|
||||
### Provider Location:
|
||||
- Place providers in `lib/features/{feature}/providers/`
|
||||
- Keep provider logic separate from UI
|
||||
- Group related providers together
|
||||
|
||||
### Error Handling:
|
||||
```dart
|
||||
class DataLoader extends AsyncNotifier<Data> {
|
||||
@override
|
||||
Future<Data> build() async {
|
||||
try {
|
||||
return await fetchData();
|
||||
} catch (e, stack) {
|
||||
// Log error
|
||||
print('Failed to load data: $e');
|
||||
// Rethrow for Riverpod to handle
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> retry() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => fetchData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using ref.read vs ref.watch:
|
||||
```dart
|
||||
// Use ref.watch in build methods (reactive)
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(counterProvider); // Rebuilds when changes
|
||||
return Text('$count');
|
||||
}
|
||||
|
||||
// Use ref.read in event handlers (one-time read)
|
||||
onPressed: () {
|
||||
ref.read(counterProvider.notifier).increment(); // Just reads once
|
||||
}
|
||||
|
||||
// Use ref.listen for side effects
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listen(authProvider, (previous, next) {
|
||||
// React to auth state changes
|
||||
if (next.value == null) {
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes:
|
||||
|
||||
### Riverpod 3.0 Changes:
|
||||
- **Unified Ref**: No more specialized ref types (just `Ref`)
|
||||
- **Simplified Notifier**: No more separate Family/AutoDispose variants
|
||||
- **Automatic Retry**: Failed providers automatically retry with backoff
|
||||
- **ref.mounted**: Check if provider is still alive after async operations
|
||||
|
||||
### Migration from StateNotifier:
|
||||
```dart
|
||||
// Old (StateNotifier)
|
||||
class CounterNotifier extends StateNotifier<int> {
|
||||
CounterNotifier() : super(0);
|
||||
void increment() => state++;
|
||||
}
|
||||
|
||||
final counterProvider = StateNotifierProvider<CounterNotifier, int>(
|
||||
(ref) => CounterNotifier(),
|
||||
);
|
||||
|
||||
// New (Notifier)
|
||||
class Counter extends Notifier<int> {
|
||||
@override
|
||||
int build() => 0;
|
||||
void increment() => state++;
|
||||
}
|
||||
|
||||
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
|
||||
```
|
||||
|
||||
### Performance Tips:
|
||||
- Use `.select()` to minimize rebuilds
|
||||
- Use `autoDispose` for temporary data
|
||||
- Implement proper `==` and `hashCode` for state classes
|
||||
- Keep state immutable
|
||||
- Use `const` constructors where possible
|
||||
@@ -31,11 +31,11 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ class ApiConstants {
|
||||
/// Use: '${ApiConstants.categories}/:id'
|
||||
static String categoryById(String id) => '$categories/$id';
|
||||
|
||||
/// GET - Fetch category with its products
|
||||
/// Use: '${ApiConstants.categories}/:id/products'
|
||||
static String categoryWithProducts(String id) => '$categories/$id/products';
|
||||
|
||||
/// POST - Sync categories (bulk update/create)
|
||||
static const String syncCategories = '$categories/sync';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
/// - Storage: Secure storage, database
|
||||
/// - Theme: Material 3 theme, colors, typography
|
||||
/// - Utils: Formatters, validators, extensions, helpers
|
||||
/// - DI: Dependency injection setup
|
||||
/// - Providers: Riverpod providers for core dependencies
|
||||
/// - Widgets: Reusable UI components
|
||||
/// - Errors: Exception and failure handling
|
||||
library;
|
||||
@@ -23,7 +23,6 @@ library;
|
||||
export 'config/config.dart';
|
||||
export 'constants/constants.dart';
|
||||
export 'database/database.dart';
|
||||
export 'di/di.dart';
|
||||
export 'errors/errors.dart';
|
||||
export 'network/network.dart';
|
||||
export 'performance.dart';
|
||||
|
||||
@@ -19,6 +19,7 @@ class SeedData {
|
||||
color: '#2196F3', // Blue
|
||||
productCount: 0,
|
||||
createdAt: now.subtract(const Duration(days: 60)),
|
||||
updatedAt: now.subtract(const Duration(days: 60)),
|
||||
),
|
||||
CategoryModel(
|
||||
id: 'cat_appliances',
|
||||
@@ -28,6 +29,7 @@ class SeedData {
|
||||
color: '#4CAF50', // Green
|
||||
productCount: 0,
|
||||
createdAt: now.subtract(const Duration(days: 55)),
|
||||
updatedAt: now.subtract(const Duration(days: 55)),
|
||||
),
|
||||
CategoryModel(
|
||||
id: 'cat_sports',
|
||||
@@ -37,6 +39,7 @@ class SeedData {
|
||||
color: '#FF9800', // Orange
|
||||
productCount: 0,
|
||||
createdAt: now.subtract(const Duration(days: 50)),
|
||||
updatedAt: now.subtract(const Duration(days: 50)),
|
||||
),
|
||||
CategoryModel(
|
||||
id: 'cat_fashion',
|
||||
@@ -46,6 +49,7 @@ class SeedData {
|
||||
color: '#E91E63', // Pink
|
||||
productCount: 0,
|
||||
createdAt: now.subtract(const Duration(days: 45)),
|
||||
updatedAt: now.subtract(const Duration(days: 45)),
|
||||
),
|
||||
CategoryModel(
|
||||
id: 'cat_books',
|
||||
@@ -55,6 +59,7 @@ class SeedData {
|
||||
color: '#9C27B0', // Purple
|
||||
productCount: 0,
|
||||
createdAt: now.subtract(const Duration(days: 40)),
|
||||
updatedAt: now.subtract(const Duration(days: 40)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/// Export all dependency injection components
|
||||
///
|
||||
/// Contains service locator and injection container setup
|
||||
library;
|
||||
|
||||
export 'injection_container.dart';
|
||||
export 'service_locator.dart';
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../features/auth/data/datasources/auth_remote_datasource.dart';
|
||||
import '../../features/auth/data/repositories/auth_repository_impl.dart';
|
||||
import '../../features/auth/domain/repositories/auth_repository.dart';
|
||||
import '../network/dio_client.dart';
|
||||
import '../network/network_info.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
/// Service locator instance
|
||||
final sl = GetIt.instance;
|
||||
|
||||
/// Initialize all dependencies
|
||||
///
|
||||
/// This function registers all the dependencies required by the app
|
||||
/// in the GetIt service locator. Call this in main() before runApp().
|
||||
Future<void> initDependencies() async {
|
||||
// ===== Core =====
|
||||
|
||||
// Connectivity (external) - Register first as it's a dependency
|
||||
sl.registerLazySingleton<Connectivity>(
|
||||
() => Connectivity(),
|
||||
);
|
||||
|
||||
// Network Info
|
||||
sl.registerLazySingleton<NetworkInfo>(
|
||||
() => NetworkInfo(sl()),
|
||||
);
|
||||
|
||||
// Dio Client
|
||||
sl.registerLazySingleton<DioClient>(
|
||||
() => DioClient(),
|
||||
);
|
||||
|
||||
// Secure Storage
|
||||
sl.registerLazySingleton<SecureStorage>(
|
||||
() => SecureStorage(),
|
||||
);
|
||||
|
||||
// ===== Authentication Feature =====
|
||||
|
||||
// Auth Remote Data Source
|
||||
sl.registerLazySingleton<AuthRemoteDataSource>(
|
||||
() => AuthRemoteDataSourceImpl(dioClient: sl()),
|
||||
);
|
||||
|
||||
// Auth Repository
|
||||
sl.registerLazySingleton<AuthRepository>(
|
||||
() => AuthRepositoryImpl(
|
||||
remoteDataSource: sl(),
|
||||
secureStorage: sl(),
|
||||
dioClient: sl(),
|
||||
),
|
||||
);
|
||||
|
||||
// ===== Data Sources =====
|
||||
// Note: Other data sources are managed by Riverpod providers
|
||||
// No direct registration needed here
|
||||
|
||||
// ===== Repositories =====
|
||||
// TODO: Register other repositories when they are implemented
|
||||
|
||||
// ===== Use Cases =====
|
||||
// TODO: Register use cases when they are implemented
|
||||
|
||||
// ===== Providers (Riverpod) =====
|
||||
// Note: Riverpod providers are registered differently
|
||||
// This is just for dependency injection of external dependencies
|
||||
}
|
||||
|
||||
/// Clear all dependencies (useful for testing)
|
||||
void resetDependencies() {
|
||||
sl.reset();
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import '../network/dio_client.dart';
|
||||
import '../network/network_info.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
/// Setup dependency injection
|
||||
Future<void> setupServiceLocator() async {
|
||||
// External dependencies
|
||||
getIt.registerLazySingleton(() => Connectivity());
|
||||
|
||||
// Core
|
||||
getIt.registerLazySingleton(() => DioClient());
|
||||
getIt.registerLazySingleton(() => NetworkInfo(getIt()));
|
||||
|
||||
// Data sources - to be added when features are implemented
|
||||
|
||||
// Repositories - to be added when features are implemented
|
||||
|
||||
// Use cases - to be added when features are implemented
|
||||
}
|
||||
104
lib/core/network/api_response.dart
Normal file
104
lib/core/network/api_response.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
/// Generic API Response wrapper
|
||||
///
|
||||
/// Wraps all API responses in a consistent format with success status,
|
||||
/// data payload, optional message, and optional pagination metadata.
|
||||
class ApiResponse<T> {
|
||||
final bool success;
|
||||
final T data;
|
||||
final String? message;
|
||||
final PaginationMeta? meta;
|
||||
|
||||
const ApiResponse({
|
||||
required this.success,
|
||||
required this.data,
|
||||
this.message,
|
||||
this.meta,
|
||||
});
|
||||
|
||||
/// Create from JSON with a data parser function
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic) dataParser,
|
||||
) {
|
||||
return ApiResponse(
|
||||
success: json['success'] as bool? ?? false,
|
||||
data: dataParser(json['data']),
|
||||
message: json['message'] as String?,
|
||||
meta: json['meta'] != null
|
||||
? PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson(dynamic Function(T) dataSerializer) {
|
||||
return {
|
||||
'success': success,
|
||||
'data': dataSerializer(data),
|
||||
if (message != null) 'message': message,
|
||||
if (meta != null) 'meta': meta!.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Pagination metadata
|
||||
class PaginationMeta {
|
||||
final int page;
|
||||
final int limit;
|
||||
final int total;
|
||||
final int totalPages;
|
||||
final bool hasPreviousPage;
|
||||
final bool hasNextPage;
|
||||
|
||||
const PaginationMeta({
|
||||
required this.page,
|
||||
required this.limit,
|
||||
required this.total,
|
||||
required this.totalPages,
|
||||
required this.hasPreviousPage,
|
||||
required this.hasNextPage,
|
||||
});
|
||||
|
||||
/// Create from JSON
|
||||
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
|
||||
return PaginationMeta(
|
||||
page: json['page'] as int,
|
||||
limit: json['limit'] as int,
|
||||
total: json['total'] as int,
|
||||
totalPages: json['totalPages'] as int,
|
||||
hasPreviousPage: json['hasPreviousPage'] as bool,
|
||||
hasNextPage: json['hasNextPage'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total': total,
|
||||
'totalPages': totalPages,
|
||||
'hasPreviousPage': hasPreviousPage,
|
||||
'hasNextPage': hasNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a copy with updated fields
|
||||
PaginationMeta copyWith({
|
||||
int? page,
|
||||
int? limit,
|
||||
int? total,
|
||||
int? totalPages,
|
||||
bool? hasPreviousPage,
|
||||
bool? hasNextPage,
|
||||
}) {
|
||||
return PaginationMeta(
|
||||
page: page ?? this.page,
|
||||
limit: limit ?? this.limit,
|
||||
total: total ?? this.total,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
hasPreviousPage: hasPreviousPage ?? this.hasPreviousPage,
|
||||
hasNextPage: hasNextPage ?? this.hasNextPage,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@
|
||||
library;
|
||||
|
||||
export 'api_interceptor.dart';
|
||||
export 'api_response.dart';
|
||||
export 'dio_client.dart';
|
||||
export 'network_info.dart';
|
||||
|
||||
23
lib/core/providers/core_providers.dart
Normal file
23
lib/core/providers/core_providers.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../network/dio_client.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
part 'core_providers.g.dart';
|
||||
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
@Riverpod(keepAlive: true)
|
||||
DioClient dioClient(Ref ref) {
|
||||
return DioClient();
|
||||
}
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
/// This is the global secure storage used for storing sensitive data like tokens.
|
||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
|
||||
@Riverpod(keepAlive: true)
|
||||
SecureStorage secureStorage(Ref ref) {
|
||||
return SecureStorage();
|
||||
}
|
||||
119
lib/core/providers/core_providers.g.dart
Normal file
119
lib/core/providers/core_providers.g.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'core_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
|
||||
@ProviderFor(dioClient)
|
||||
const dioClientProvider = DioClientProvider._();
|
||||
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
|
||||
final class DioClientProvider
|
||||
extends $FunctionalProvider<DioClient, DioClient, DioClient>
|
||||
with $Provider<DioClient> {
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
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';
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
/// This is the global secure storage used for storing sensitive data like tokens.
|
||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
|
||||
|
||||
@ProviderFor(secureStorage)
|
||||
const secureStorageProvider = SecureStorageProvider._();
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
/// This is the global secure storage used for storing sensitive data like tokens.
|
||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
|
||||
|
||||
final class SecureStorageProvider
|
||||
extends $FunctionalProvider<SecureStorage, SecureStorage, SecureStorage>
|
||||
with $Provider<SecureStorage> {
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
/// This is the global secure storage used for storing sensitive data like tokens.
|
||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
|
||||
const SecureStorageProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'secureStorageProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$secureStorageHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<SecureStorage> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
SecureStorage create(Ref ref) {
|
||||
return secureStorage(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SecureStorage value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SecureStorage>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$secureStorageHash() => r'5c9908c0046ad0e39469ee7acbb5540397b36693';
|
||||
@@ -1,3 +1,4 @@
|
||||
/// Export all core providers
|
||||
export 'core_providers.dart';
|
||||
export 'network_info_provider.dart';
|
||||
export 'sync_status_provider.dart';
|
||||
|
||||
@@ -45,10 +45,10 @@ class SyncStatus extends _$SyncStatus {
|
||||
|
||||
try {
|
||||
// Sync categories first (products depend on categories)
|
||||
await ref.read(categoriesProvider.notifier).syncCategories();
|
||||
await ref.read(categoriesProvider.notifier).refresh();
|
||||
|
||||
// Then sync products
|
||||
await ref.read(productsProvider.notifier).syncProducts();
|
||||
await ref.read(productsProvider.notifier).refresh();
|
||||
|
||||
// Update last sync time in settings
|
||||
await ref.read(settingsProvider.notifier).updateLastSyncTime();
|
||||
@@ -100,7 +100,7 @@ class SyncStatus extends _$SyncStatus {
|
||||
);
|
||||
|
||||
try {
|
||||
await ref.read(productsProvider.notifier).syncProducts();
|
||||
await ref.read(productsProvider.notifier).refresh();
|
||||
await ref.read(settingsProvider.notifier).updateLastSyncTime();
|
||||
|
||||
state = AsyncValue.data(
|
||||
@@ -146,7 +146,7 @@ class SyncStatus extends _$SyncStatus {
|
||||
);
|
||||
|
||||
try {
|
||||
await ref.read(categoriesProvider.notifier).syncCategories();
|
||||
await ref.read(categoriesProvider.notifier).refresh();
|
||||
await ref.read(settingsProvider.notifier).updateLastSyncTime();
|
||||
|
||||
state = AsyncValue.data(
|
||||
|
||||
@@ -36,7 +36,7 @@ final class SyncStatusProvider
|
||||
SyncStatus create() => SyncStatus();
|
||||
}
|
||||
|
||||
String _$syncStatusHash() => r'dc92a1b83c89af94dfe94b646aa81d9501f371d7';
|
||||
String _$syncStatusHash() => r'bf09683a3a67b6c7104274c7a4b92ee410de8e45';
|
||||
|
||||
/// Sync status provider - manages data synchronization state
|
||||
|
||||
|
||||
@@ -448,20 +448,20 @@ class ErrorHandlingExample extends ConsumerWidget {
|
||||
|
||||
void nonWidgetExample() {
|
||||
// If you need to access auth outside widgets (e.g., in services),
|
||||
// use the service locator directly:
|
||||
// you can pass WidgetRef as a parameter or use ProviderContainer:
|
||||
|
||||
// import 'package:retail/core/di/injection_container.dart';
|
||||
// import 'package:retail/features/auth/domain/repositories/auth_repository.dart';
|
||||
|
||||
// final authRepository = sl<AuthRepository>();
|
||||
//
|
||||
// // Check if authenticated
|
||||
// Method 1: Pass WidgetRef as parameter
|
||||
// Future<void> myService(WidgetRef ref) async {
|
||||
// final authRepository = ref.read(authRepositoryProvider);
|
||||
// final isAuthenticated = await authRepository.isAuthenticated();
|
||||
//
|
||||
// // Get token
|
||||
// final token = await authRepository.getAccessToken();
|
||||
//
|
||||
// print('Token: $token');
|
||||
// print('Is authenticated: $isAuthenticated');
|
||||
// }
|
||||
|
||||
// Method 2: Use ProviderContainer (for non-Flutter code)
|
||||
// final container = ProviderContainer();
|
||||
// final authRepository = container.read(authRepositoryProvider);
|
||||
// final isAuthenticated = await authRepository.isAuthenticated();
|
||||
// container.dispose(); // Don't forget to dispose!
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -477,7 +477,9 @@ void tokenInjectionExample() {
|
||||
// You don't need to manually add the token - it's automatic!
|
||||
|
||||
// Example of making an API call after login:
|
||||
// final response = await sl<DioClient>().get('/api/products');
|
||||
// Using Riverpod:
|
||||
// final dioClient = ref.read(dioClientProvider);
|
||||
// final response = await dioClient.get('/api/products');
|
||||
//
|
||||
// The above request will automatically include:
|
||||
// Headers: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../core/storage/secure_storage.dart';
|
||||
import '../../../../core/providers/providers.dart';
|
||||
import '../../data/datasources/auth_remote_datasource.dart';
|
||||
import '../../data/repositories/auth_repository_impl.dart';
|
||||
import '../../domain/entities/user.dart';
|
||||
@@ -8,18 +7,6 @@ import '../../domain/repositories/auth_repository.dart';
|
||||
|
||||
part 'auth_provider.g.dart';
|
||||
|
||||
/// Provider for DioClient (singleton)
|
||||
@Riverpod(keepAlive: true)
|
||||
DioClient dioClient(Ref ref) {
|
||||
return DioClient();
|
||||
}
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
@Riverpod(keepAlive: true)
|
||||
SecureStorage secureStorage(Ref ref) {
|
||||
return SecureStorage();
|
||||
}
|
||||
|
||||
/// Provider for AuthRemoteDataSource
|
||||
@Riverpod(keepAlive: true)
|
||||
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
|
||||
|
||||
@@ -8,98 +8,6 @@ part of 'auth_provider.dart';
|
||||
|
||||
// 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';
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
|
||||
@ProviderFor(secureStorage)
|
||||
const secureStorageProvider = SecureStorageProvider._();
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
|
||||
final class SecureStorageProvider
|
||||
extends $FunctionalProvider<SecureStorage, SecureStorage, SecureStorage>
|
||||
with $Provider<SecureStorage> {
|
||||
/// Provider for SecureStorage (singleton)
|
||||
const SecureStorageProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'secureStorageProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$secureStorageHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<SecureStorage> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
SecureStorage create(Ref ref) {
|
||||
return secureStorage(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SecureStorage value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SecureStorage>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$secureStorageHash() => r'5c9908c0046ad0e39469ee7acbb5540397b36693';
|
||||
|
||||
/// Provider for AuthRemoteDataSource
|
||||
|
||||
@ProviderFor(authRemoteDataSource)
|
||||
@@ -234,7 +142,7 @@ final class AuthProvider extends $NotifierProvider<Auth, AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$authHash() => r'4b053a7691f573316a8957577dd27a3ed73d89be';
|
||||
String _$authHash() => r'73c9e7b70799eba2904eb6fc65454332d4146a33';
|
||||
|
||||
/// Auth state notifier provider
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/category_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';
|
||||
|
||||
/// Category remote data source using API
|
||||
abstract class CategoryRemoteDataSource {
|
||||
/// Get all categories (public endpoint - no auth required)
|
||||
Future<List<CategoryModel>> getAllCategories();
|
||||
|
||||
/// Get single category by ID (public endpoint - no auth required)
|
||||
Future<CategoryModel> getCategoryById(String id);
|
||||
|
||||
/// Get category with its products with pagination (public endpoint)
|
||||
/// Returns Map with 'category' and 'products' with pagination info
|
||||
Future<Map<String, dynamic>> getCategoryWithProducts(
|
||||
String id,
|
||||
int page,
|
||||
int limit,
|
||||
);
|
||||
}
|
||||
|
||||
class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
|
||||
final DioClient client;
|
||||
|
||||
CategoryRemoteDataSourceImpl(this.client);
|
||||
|
||||
@override
|
||||
Future<List<CategoryModel>> getAllCategories() async {
|
||||
try {
|
||||
final response = await client.get(ApiConstants.categories);
|
||||
|
||||
// Parse API response using ApiResponse model
|
||||
final apiResponse = ApiResponse<List<CategoryModel>>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => (data as List<dynamic>)
|
||||
.map((json) => CategoryModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch categories',
|
||||
);
|
||||
}
|
||||
|
||||
return apiResponse.data;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to fetch categories: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CategoryModel> getCategoryById(String id) async {
|
||||
try {
|
||||
final response = await client.get(ApiConstants.categoryById(id));
|
||||
|
||||
// Parse API response using ApiResponse model
|
||||
final apiResponse = ApiResponse<CategoryModel>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => CategoryModel.fromJson(data as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch category',
|
||||
);
|
||||
}
|
||||
|
||||
return apiResponse.data;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to fetch category: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getCategoryWithProducts(
|
||||
String id,
|
||||
int page,
|
||||
int limit,
|
||||
) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
'${ApiConstants.categories}/$id/products',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
|
||||
// Parse API response - data contains category with nested products
|
||||
final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => data as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch category with products',
|
||||
);
|
||||
}
|
||||
|
||||
final responseData = apiResponse.data;
|
||||
|
||||
// Extract category info (excluding products array)
|
||||
final categoryData = Map<String, dynamic>.from(responseData);
|
||||
final products = categoryData.remove('products') as List<dynamic>? ?? [];
|
||||
|
||||
// Create category model from remaining data
|
||||
final category = CategoryModel.fromJson(categoryData);
|
||||
|
||||
return {
|
||||
'category': category,
|
||||
'products': products,
|
||||
'meta': apiResponse.meta?.toJson() ?? {},
|
||||
};
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to fetch category with products: $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'] ?? 'Category 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/// Export all categories data sources
|
||||
///
|
||||
/// Contains local data sources for categories
|
||||
/// Contains local and remote data sources for categories
|
||||
library;
|
||||
|
||||
export 'category_local_datasource.dart';
|
||||
export 'category_remote_datasource.dart';
|
||||
|
||||
@@ -27,6 +27,9 @@ class CategoryModel extends HiveObject {
|
||||
@HiveField(6)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(7)
|
||||
final DateTime updatedAt;
|
||||
|
||||
CategoryModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -35,6 +38,7 @@ class CategoryModel extends HiveObject {
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
@@ -47,6 +51,7 @@ class CategoryModel extends HiveObject {
|
||||
color: color,
|
||||
productCount: productCount,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,6 +65,7 @@ class CategoryModel extends HiveObject {
|
||||
color: category.color,
|
||||
productCount: category.productCount,
|
||||
createdAt: category.createdAt,
|
||||
updatedAt: category.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,8 +77,9 @@ class CategoryModel extends HiveObject {
|
||||
description: json['description'] as String?,
|
||||
iconPath: json['iconPath'] as String?,
|
||||
color: json['color'] as String?,
|
||||
productCount: json['productCount'] as int,
|
||||
productCount: json['productCount'] as int? ?? 0,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,6 +93,7 @@ class CategoryModel extends HiveObject {
|
||||
'color': color,
|
||||
'productCount': productCount,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,6 +106,7 @@ class CategoryModel extends HiveObject {
|
||||
String? color,
|
||||
int? productCount,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return CategoryModel(
|
||||
id: id ?? this.id,
|
||||
@@ -107,6 +116,7 @@ class CategoryModel extends HiveObject {
|
||||
color: color ?? this.color,
|
||||
productCount: productCount ?? this.productCount,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
color: fields[4] as String?,
|
||||
productCount: (fields[5] as num).toInt(),
|
||||
createdAt: fields[6] as DateTime,
|
||||
updatedAt: fields[7] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CategoryModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -44,7 +45,9 @@ class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
..writeByte(5)
|
||||
..write(obj.productCount)
|
||||
..writeByte(6)
|
||||
..write(obj.createdAt);
|
||||
..write(obj.createdAt)
|
||||
..writeByte(7)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,6 +9,7 @@ class Category extends Equatable {
|
||||
final String? color;
|
||||
final int productCount;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Category({
|
||||
required this.id,
|
||||
@@ -18,6 +19,7 @@ class Category extends Equatable {
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -29,5 +31,6 @@ class Category extends Equatable {
|
||||
color,
|
||||
productCount,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 [];
|
||||
return await _fetchCategories();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for single category by ID
|
||||
@riverpod
|
||||
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
|
||||
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;
|
||||
|
||||
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 _fetchCategoryWithProducts(
|
||||
categoryId: currentState.category.id,
|
||||
page: 1,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for selected category
|
||||
/// Provider for selected category state
|
||||
/// This is used in the products feature for filtering
|
||||
@riverpod
|
||||
class SelectedCategory extends _$SelectedCategory {
|
||||
class SelectedCategoryInCategories extends _$SelectedCategoryInCategories {
|
||||
@override
|
||||
String? build() => null;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
/// 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'selectedCategoryProvider',
|
||||
name: r'categoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedCategoryHash();
|
||||
String debugGetCreateSourceHash() => _$categoryHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'categoryProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedCategory create() => SelectedCategory();
|
||||
$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'selectedCategoryInCategoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedCategoryInCategoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: (_, __) => {},
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -39,7 +39,9 @@ class ProductSelector extends ConsumerWidget {
|
||||
message: error.toString(),
|
||||
onRetry: () => ref.refresh(productsProvider),
|
||||
),
|
||||
data: (products) {
|
||||
data: (paginationState) {
|
||||
final products = paginationState.products;
|
||||
|
||||
if (products.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No products available',
|
||||
|
||||
@@ -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 {
|
||||
try {
|
||||
final response = await client.get(ApiConstants.productById(id));
|
||||
return ProductModel.fromJson(response.data);
|
||||
|
||||
// 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 {
|
||||
Future<Map<String, dynamic>> searchProducts(
|
||||
String query,
|
||||
int page,
|
||||
int limit,
|
||||
) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
ApiConstants.searchProducts,
|
||||
queryParameters: {'q': query},
|
||||
queryParameters: {
|
||||
'q': query,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
final List<dynamic> data = response.data['products'] ?? [];
|
||||
return data.map((json) => ProductModel.fromJson(json)).toList();
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_datasource_provider.dart';
|
||||
part of 'product_datasource_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
@@ -8,58 +8,58 @@ part of 'category_datasource_provider.dart';
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for category local data source
|
||||
/// Provider for product remote data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
@ProviderFor(categoryLocalDataSource)
|
||||
const categoryLocalDataSourceProvider = CategoryLocalDataSourceProvider._();
|
||||
@ProviderFor(productRemoteDataSource)
|
||||
const productRemoteDataSourceProvider = ProductRemoteDataSourceProvider._();
|
||||
|
||||
/// Provider for category local data source
|
||||
/// Provider for product remote data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
final class CategoryLocalDataSourceProvider
|
||||
final class ProductRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource
|
||||
ProductRemoteDataSource,
|
||||
ProductRemoteDataSource,
|
||||
ProductRemoteDataSource
|
||||
>
|
||||
with $Provider<CategoryLocalDataSource> {
|
||||
/// Provider for category local data source
|
||||
with $Provider<ProductRemoteDataSource> {
|
||||
/// Provider for product remote data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
const CategoryLocalDataSourceProvider._()
|
||||
const ProductRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryLocalDataSourceProvider',
|
||||
name: r'productRemoteDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryLocalDataSourceHash();
|
||||
String debugGetCreateSourceHash() => _$productRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CategoryLocalDataSource> $createElement(
|
||||
$ProviderElement<ProductRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CategoryLocalDataSource create(Ref ref) {
|
||||
return categoryLocalDataSource(ref);
|
||||
ProductRemoteDataSource create(Ref ref) {
|
||||
return productRemoteDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CategoryLocalDataSource value) {
|
||||
Override overrideWithValue(ProductRemoteDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CategoryLocalDataSource>(value),
|
||||
providerOverride: $SyncValueProvider<ProductRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryLocalDataSourceHash() =>
|
||||
r'1f8412f2dc76a348873f1da4f76ae4a08991f269';
|
||||
String _$productRemoteDataSourceHash() =>
|
||||
r'ff7a408a03041d45714a470abf3cb226b7c32b2c';
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
void clear() {
|
||||
state = '';
|
||||
ref.read(productsProvider.notifier).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@ dependencies:
|
||||
equatable: ^2.0.7
|
||||
dartz: ^0.10.1
|
||||
|
||||
# Dependency Injection
|
||||
get_it: ^8.0.4
|
||||
# Note: Dependency Injection is handled by Riverpod (flutter_riverpod above)
|
||||
# No need for GetIt or other DI packages
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user