4.9 KiB
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:
- Loads favorites from API/cache
- Fetches all products
- 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
@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
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
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
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
-
lib/features/favorites/presentation/providers/favorites_provider.dart
- Added
ref.keepAlive()toFavoritesclass (line 81) - Added
ref.keepAlive()tofavoriteProductsprovider (line 271)
- Added
-
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
- No empty state flash on first load
- Smooth loading with skeleton
- Previous data shown on subsequent visits
- 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
-
Add shimmer duration control
- Minimum shimmer display time to prevent flash
- Smooth fade transition from skeleton to content
-
Progressive loading
- Show cached data first
- Overlay with "Updating..." badge
- Fade in updated items
-
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