199 lines
4.9 KiB
Markdown
199 lines
4.9 KiB
Markdown
# Favorites Page - Loading State Fix
|
|
|
|
## Problem
|
|
Users were seeing the empty state ("Chưa có sản phẩm yêu thích") flash briefly before the actual favorites data loaded, even when they had favorites. This created a poor user experience.
|
|
|
|
## Root Cause
|
|
The `favoriteProductsProvider` is an async provider that:
|
|
1. Loads favorites from API/cache
|
|
2. Fetches all products
|
|
3. Filters to get favorite products
|
|
|
|
During this process, the provider goes through these states:
|
|
- **Loading** → Returns empty list [] → Shows loading skeleton
|
|
- **Data** → If products.isEmpty → Shows empty state ❌ **FLASH**
|
|
- **Data** → Returns actual products → Shows grid
|
|
|
|
The flash happened because when the provider rebuilt, it momentarily returned an empty list before the actual data arrived.
|
|
|
|
## Solution
|
|
|
|
### 1. Added `ref.keepAlive()` to Providers
|
|
```dart
|
|
@riverpod
|
|
class Favorites extends _$Favorites {
|
|
@override
|
|
Future<Set<String>> build() async {
|
|
ref.keepAlive(); // ← Prevents provider disposal
|
|
// ... rest of code
|
|
}
|
|
}
|
|
|
|
@riverpod
|
|
Future<List<Product>> favoriteProducts(Ref ref) async {
|
|
ref.keepAlive(); // ← Keeps previous data in memory
|
|
// ... rest of code
|
|
}
|
|
```
|
|
|
|
**Benefits**:
|
|
- Prevents state from being disposed when widget rebuilds
|
|
- Keeps previous data available via `favoriteProductsAsync.valueOrNull`
|
|
- Reduces unnecessary API calls
|
|
|
|
### 2. Smart Loading State Logic
|
|
|
|
```dart
|
|
loading: () {
|
|
// 1. Check for previous data first
|
|
final previousValue = favoriteProductsAsync.valueOrNull;
|
|
|
|
// 2. If we have previous data, show it while loading
|
|
if (previousValue != null && previousValue.isNotEmpty) {
|
|
return Stack([
|
|
_FavoritesGrid(products: previousValue), // Show old data
|
|
LoadingIndicator(), // Small loading badge on top
|
|
]);
|
|
}
|
|
|
|
// 3. Use favoriteCount as a hint
|
|
if (favoriteCount > 0) {
|
|
return LoadingState(); // Show skeleton
|
|
}
|
|
|
|
// 4. No data, show skeleton (not empty state)
|
|
return LoadingState();
|
|
}
|
|
```
|
|
|
|
### 3. Data State - Only Show Empty When Actually Empty
|
|
|
|
```dart
|
|
data: (products) {
|
|
// Only show empty state when data is actually empty
|
|
if (products.isEmpty) {
|
|
return const _EmptyState();
|
|
}
|
|
|
|
return RefreshIndicator(
|
|
child: _FavoritesGrid(products: products),
|
|
);
|
|
}
|
|
```
|
|
|
|
## User Experience Flow
|
|
|
|
### Before Fix ❌
|
|
```
|
|
User opens favorites page
|
|
↓
|
|
Loading skeleton shows (0.1s)
|
|
↓
|
|
Empty state flashes (0.2s) ⚡ BAD UX
|
|
↓
|
|
Favorites grid appears
|
|
```
|
|
|
|
### After Fix ✅
|
|
```
|
|
User opens favorites page
|
|
↓
|
|
Loading skeleton shows (0.3s)
|
|
↓
|
|
Favorites grid appears smoothly
|
|
```
|
|
|
|
**OR** if returning to page:
|
|
```
|
|
User opens favorites page (2nd time)
|
|
↓
|
|
Previous favorites show immediately
|
|
↓
|
|
Small "Đang tải..." badge appears at top
|
|
↓
|
|
Updated favorites appear (if changed)
|
|
```
|
|
|
|
## Additional Benefits
|
|
|
|
### 1. Better Offline Support
|
|
- When offline, previous data stays visible
|
|
- Shows error banner on top instead of hiding content
|
|
- User can still browse cached favorites
|
|
|
|
### 2. Faster Perceived Performance
|
|
- Instant display of previous data
|
|
- Users don't see empty states during reloads
|
|
- Smoother transitions
|
|
|
|
### 3. Error Handling
|
|
```dart
|
|
error: (error, stackTrace) {
|
|
final previousValue = favoriteProductsAsync.valueOrNull;
|
|
|
|
// Show previous data with error message
|
|
if (previousValue != null && previousValue.isNotEmpty) {
|
|
return Stack([
|
|
_FavoritesGrid(products: previousValue),
|
|
ErrorBanner(onRetry: ...),
|
|
]);
|
|
}
|
|
|
|
// No previous data, show full error state
|
|
return _ErrorState();
|
|
}
|
|
```
|
|
|
|
## Files Modified
|
|
|
|
1. **lib/features/favorites/presentation/providers/favorites_provider.dart**
|
|
- Added `ref.keepAlive()` to `Favorites` class (line 81)
|
|
- Added `ref.keepAlive()` to `favoriteProducts` provider (line 271)
|
|
|
|
2. **lib/features/favorites/presentation/pages/favorites_page.dart**
|
|
- Enhanced loading state logic (lines 138-193)
|
|
- Added previous value checking
|
|
- Added favoriteCount hint logic
|
|
|
|
## Testing Checklist
|
|
|
|
- [x] No empty state flash on first load
|
|
- [x] Smooth loading with skeleton
|
|
- [x] Previous data shown on subsequent visits
|
|
- [x] Loading indicator overlay when refreshing
|
|
- [ ] Test with slow network (3G)
|
|
- [ ] Test with offline mode
|
|
- [ ] Test with errors during load
|
|
|
|
## Performance Impact
|
|
|
|
✅ **Positive**:
|
|
- Reduced state rebuilds
|
|
- Better memory management with keepAlive
|
|
- Fewer API calls on navigation
|
|
|
|
⚠️ **Watch**:
|
|
- Memory usage (keepAlive keeps data in memory)
|
|
- Can manually dispose with `ref.invalidate()` if needed
|
|
|
|
## Future Improvements
|
|
|
|
1. **Add shimmer duration control**
|
|
- Minimum shimmer display time to prevent flash
|
|
- Smooth fade transition from skeleton to content
|
|
|
|
2. **Progressive loading**
|
|
- Show cached data first
|
|
- Overlay with "Updating..." badge
|
|
- Fade in updated items
|
|
|
|
3. **Prefetch on app launch**
|
|
- Load favorites in background
|
|
- Data ready before user navigates to page
|
|
|
|
---
|
|
|
|
**Status**: ✅ Implemented
|
|
**Impact**: High - Significantly improves perceived performance
|
|
**Breaking Changes**: None
|