# Cart Quantity Update Debounce Implementation ## Overview Implemented a 3-second debounce for cart quantity updates to prevent excessive API calls. UI updates happen instantly, but API sync is delayed until the user stops changing quantities. ## Problem Solved **Before**: Every increment/decrement button press triggered an immediate API call - Multiple rapid clicks = multiple API calls - Poor performance and UX - Unnecessary server load - Potential rate limiting issues **After**: UI updates instantly, API syncs after 3 seconds of inactivity - User can rapidly change quantities - Only one API call after user stops - Smooth, responsive UI - Reduced server load ## Implementation Details ### 1. Debounce Timer in Cart Provider **File**: `lib/features/cart/presentation/providers/cart_provider.dart` ```dart @Riverpod(keepAlive: true) class Cart extends _$Cart { /// Debounce timer for quantity updates (3 seconds) Timer? _debounceTimer; /// Map to track pending quantity updates (productId -> quantity) final Map _pendingQuantityUpdates = {}; @override CartState build() { // Cancel debounce timer when provider is disposed ref.onDispose(() { _debounceTimer?.cancel(); }); return CartState.initial().copyWith( memberTier: 'Diamond', memberDiscountPercent: 15.0, ); } } ``` ### 2. Local Update Method (Instant UI Update) ```dart /// Update item quantity immediately (local only, no API call) /// /// Used for instant UI updates. Actual API sync happens after debounce. void updateQuantityLocal(String productId, double newQuantity) { if (newQuantity <= 0) { removeFromCart(productId); return; } final currentState = state; final itemIndex = currentState.items.indexWhere( (item) => item.product.productId == productId, ); if (itemIndex == -1) return; final item = currentState.items[itemIndex]; // Update local state immediately (instant UI update) final converted = _calculateConversion( newQuantity, item.product.conversionOfSm, ); final updatedItems = List.from(currentState.items); updatedItems[itemIndex] = item.copyWith( quantity: newQuantity, quantityConverted: converted.convertedQuantity, boxes: converted.boxes, ); final newState = currentState.copyWith(items: updatedItems); state = _recalculateTotal(newState); // Track pending update for API sync _pendingQuantityUpdates[productId] = newQuantity; // Schedule debounced API sync _scheduleDebouncedSync(); } ``` ### 3. Debounce Scheduling ```dart /// Schedule debounced sync to API (3 seconds after last change) void _scheduleDebouncedSync() { // Cancel existing timer (restarts the 3s countdown) _debounceTimer?.cancel(); // Start new timer (3 seconds debounce) _debounceTimer = Timer(const Duration(seconds: 3), () { _syncPendingQuantityUpdates(); }); } ``` ### 4. Background API Sync ```dart /// Sync all pending quantity updates to API Future _syncPendingQuantityUpdates() async { if (_pendingQuantityUpdates.isEmpty) return; final repository = await ref.read(cartRepositoryProvider.future); final currentState = state; // Create a copy of pending updates final updates = Map.from(_pendingQuantityUpdates); _pendingQuantityUpdates.clear(); // Sync each update to API (background, no loading state) for (final entry in updates.entries) { final productId = entry.key; final quantity = entry.value; final item = currentState.items.firstWhere( (item) => item.product.productId == productId, orElse: () => throw Exception('Item not found'), ); try { await repository.updateQuantity( itemId: item.product.erpnextItemCode ?? productId, quantity: quantity, price: item.product.basePrice, ); } catch (e) { // Silent fail - keep local state, user can retry later print('[Cart] Failed to sync quantity for $productId: $e'); } } } ``` ### 5. Updated Increment/Decrement Methods ```dart /// Increment quantity (with debounce) /// /// Updates UI immediately, syncs to API after 3s of no changes. void incrementQuantity(String productId) { final currentState = state; final item = currentState.items.firstWhere( (item) => item.product.productId == productId, ); updateQuantityLocal(productId, item.quantity + 1); } /// Decrement quantity (minimum 1, with debounce) /// /// Updates UI immediately, syncs to API after 3s of no changes. void decrementQuantity(String productId) { final currentState = state; final item = currentState.items.firstWhere( (item) => item.product.productId == productId, ); // Keep minimum quantity at 1, don't go to 0 if (item.quantity > 1) { updateQuantityLocal(productId, item.quantity - 1); } } ``` ### 6. Force Sync on Navigation & Checkout **File**: `lib/features/cart/presentation/pages/cart_page.dart` #### A. Force Sync on Page Disposal ```dart class _CartPageState extends ConsumerState { @override void dispose() { // Force sync any pending quantity updates before leaving cart page ref.read(cartProvider.notifier).forceSyncPendingUpdates(); super.dispose(); } } ``` #### B. Force Sync on Checkout Button (Skip Debounce) ⚡ ```dart class _CartPageState extends ConsumerState { bool _isSyncing = false; @override Widget build(BuildContext context) { return ElevatedButton( onPressed: hasSelection && !_isSyncing ? () async { // Set syncing state (show loading) setState(() { _isSyncing = true; }); // Force sync immediately - NO WAITING for debounce! await ref .read(cartProvider.notifier) .forceSyncPendingUpdates(); // Reset syncing state if (mounted) { setState(() { _isSyncing = false; }); // Navigate to checkout with synced data context.push(RouteNames.checkout); } } : null, child: _isSyncing ? CircularProgressIndicator() // Show loading while syncing : Text('Tiến hành đặt hàng'), ); } } ``` **Provider Method**: ```dart /// Force sync all pending quantity updates immediately /// /// Useful when: /// - User taps checkout button (skip 3s debounce) /// - User navigates away or closes cart /// - Need to ensure data is synced before critical operations Future forceSyncPendingUpdates() async { _debounceTimer?.cancel(); await _syncPendingQuantityUpdates(); } ``` ## User Flow ### Scenario 1: Rapid Clicks (Debounced) ``` User clicks +5 times rapidly (within 3 seconds) ↓ Each click: UI updates instantly (1→2→3→4→5) ↓ Timer restarts on each click ↓ User stops clicking ↓ 3 seconds pass ↓ Single API call: updateQuantity(productId, 5) ``` ### Scenario 2: Manual Text Input (Immediate) ``` User types "10" in quantity field ↓ User presses Enter ↓ Immediate API call: updateQuantity(productId, 10) ↓ No debounce (direct input needs immediate sync) ``` ### Scenario 3: Navigate Away (Force Sync) ``` User clicks + button 3 times ↓ UI updates: 1→2→3 ↓ Timer is running (1 second passed) ↓ User navigates back ↓ dispose() called ↓ forceSyncPendingUpdates() executes ↓ Immediate API call: updateQuantity(productId, 3) ``` ### Scenario 4: Checkout Button (Force Sync - Skip Debounce) ⚡ NEW ``` User clicks + button 5 times ↓ UI updates: 1→2→3→4→5 ↓ Timer is running (1 second passed, would wait 2 more seconds) ↓ User clicks "Tiến hành đặt hàng" (Checkout) ↓ Button shows loading spinner ↓ forceSyncPendingUpdates() called IMMEDIATELY ↓ Debounce timer cancelled ↓ API call: updateQuantity(productId, 5) - NO WAITING! ↓ Navigate to checkout page with synced data ✅ ``` ## Benefits ✅ **Instant UI feedback** - No waiting for API responses ✅ **Reduced API calls** - Only 1 call per product after changes stop ✅ **Better UX** - Smooth, responsive interface ✅ **Server-friendly** - Minimizes unnecessary requests ✅ **Offline-ready** - Local state updates work offline ✅ **Force sync on exit** - Ensures changes are saved ✅ **Skip debounce on checkout** - Immediate sync when user clicks checkout ⚡ NEW ## Configuration ### Debounce Duration Default: **3 seconds** ✅ To change: ```dart _debounceTimer = Timer(const Duration(seconds: 3), () { _syncPendingQuantityUpdates(); }); ``` Recommended values: - **2-3 seconds**: Responsive, good balance (current setting) ✅ - **5 seconds**: More conservative (fewer API calls) - **1 second**: Very aggressive (more API calls, but faster sync) ## Testing ### Manual Testing 1. **Test rapid clicks**: - Open cart - Click + button 10 times rapidly - Watch console: Should see only 1 API call after 3s 2. **Test text input**: - Type quantity directly - Press Enter - Should see immediate API call 3. **Test navigation sync**: - Click + button 3 times - Immediately navigate back - Should see API call before page closes 4. **Test multiple products**: - Change quantity on product A - Change quantity on product B - Wait 3 seconds - Should batch update both products 5. **Test checkout force sync** ⚡ NEW: - Click + button 5 times rapidly - Immediately click "Tiến hành đặt hàng" (within 3s) - Button should show loading spinner - API call should happen immediately (skip debounce) - Should navigate to checkout with synced data ### Expected Behavior ``` // Rapid increments (debounced) Click +1 → UI: 2, API: none Click +1 → UI: 3, API: none Click +1 → UI: 4, API: none Wait 3s → UI: 4, API: updateQuantity(4) ✅ // Direct input (immediate) Type "10" → UI: 10, API: none Press Enter → UI: 10, API: updateQuantity(10) ✅ // Navigate away (force sync) Click +1 → UI: 2, API: none Navigate back → UI: 2, API: updateQuantity(2) ✅ // Checkout button (force sync - skip debounce) ⚡ NEW Click +5 times → UI: 1→2→3→4→5, API: none Click checkout (after 1s) → Loading spinner shown → API: updateQuantity(5) IMMEDIATELY (skip remaining 2s debounce) → Navigate to checkout ✅ ``` ## Error Handling ### API Sync Failure - Local state is preserved - User sees correct quantity in UI - Error is logged silently - User can retry by refreshing cart ### Offline Behavior - All updates work in local state - API calls fail silently - TODO: Add to offline queue for retry when online ## Performance Impact ### Before Debounce - 10 rapid clicks = 10 API calls - Each call takes ~200-500ms - Total time: 2-5 seconds of loading - Poor UX, server strain ### After Debounce - 10 rapid clicks = 1 API call (after 3s) - UI updates are instant (<16ms per frame) - Total time: 3 seconds wait + 1 API call - Great UX, minimal server load ## Future Enhancements 1. **Batch Updates**: Combine multiple product updates into single API call 2. **Offline Queue**: Persist pending updates to Hive for offline resilience 3. **Visual Indicator**: Show "syncing..." badge when pending updates exist 4. **Configurable Timeout**: Allow users to adjust debounce duration 5. **Smart Sync**: Sync immediately before checkout/payment ## Related Files - **Cart Provider**: `lib/features/cart/presentation/providers/cart_provider.dart` - **Cart Page**: `lib/features/cart/presentation/pages/cart_page.dart` - **Cart Item Widget**: `lib/features/cart/presentation/widgets/cart_item_widget.dart` - **Cart Repository**: `lib/features/cart/data/repositories/cart_repository_impl.dart` ## Summary The debounce implementation provides a smooth, responsive cart experience while minimizing server load. Users get instant feedback, and the app intelligently batches API calls. This is a best practice for any real-time data synchronization scenario! 🎉