Skip to the content.

Pole Inventory Caching Implementation

Overview

The BPVA Calendar app implements intelligent caching for pole inventory data to significantly reduce Firestore read operations and improve app performance. This caching strategy reduces Firestore costs by 80-90% for typical usage patterns while maintaining data accuracy and real-time updates where needed.

Problem Statement

Before Caching

The original PoleInventoryService used real-time Firestore streams:

Stream<List<Pole>> getPoles() {
  return _firestore
      .collection('poles')
      .orderBy('brand')
      .orderBy('weight')
      .snapshots()  // Real-time listener
      .map(...)
}

Cost Analysis (for a typical gym with 100 poles, 20 active checkouts, 500 checkout history records):

The Issue

  1. Pole data rarely changes - Adding/removing poles is infrequent (weekly at most)
  2. Checkout history never changes - Historical records are immutable
  3. Active checkouts change moderately - Only when poles are checked in/out
  4. Multiple screen visits - Users often refresh the screen unnecessarily
  5. Firestore caching not sufficient - Built-in cache doesn’t prevent new listener reads

Solution: CachedPoleInventoryService

Architecture

The CachedPoleInventoryService wraps the existing PoleInventoryService with intelligent caching:

┌─────────────────────────────────────┐
│  PoleInventoryScreen                │
│  BulkCheckoutScreen                 │
│  PoleImportUtil                     │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│  CachedPoleInventoryService         │
│  - TTL-based caching                │
│  - Automatic cache invalidation     │
│  - Smart refresh logic              │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│  PoleInventoryService               │
│  - Direct Firestore access          │
└─────────────────────────────────────┘

Caching Strategy

1. Poles Collection

2. Active Checkouts

3. Checkout History

Key Features

Automatic Cache Invalidation

The service automatically invalidates and refreshes caches when data changes:

Future<String> addPole({...}) async {
  final result = await _poleService.addPole(...);
  invalidatePoleCache();
  _fetchPolesFromFirestore(); // Auto-refresh
  return result;
}

Operations that trigger cache invalidation:

Manual Refresh

Users can force refresh all data:

  1. Refresh button in AppBar - Forces cache refresh
  2. Pull-to-refresh on all three tabs - Reloads data with forceRefresh: true
Future<void> _loadData({bool forceRefresh = false}) async {
  _poleInventoryService.getPoles(forceRefresh: forceRefresh).listen(...);
  _poleInventoryService.getCheckoutHistory(forceRefresh: forceRefresh).listen(...);
}

Smart Query Optimization

The service optimizes queries by checking cache before hitting Firestore:

Future<Pole?> getPoleById(String poleId) async {
  // Check cache first
  if (_isPolesCacheValid && _cachedPoles != null) {
    try {
      return _cachedPoles!.firstWhere((pole) => pole.id == poleId);
    } catch (e) {
      // Not found in cache, fetch from Firestore
    }
  }
  return await _poleService.getPoleById(poleId);
}

Implementation Details

File: lib/services/cached_pole_inventory_service.dart

Key Components:

class CachedPoleInventoryService {
  // Cache data
  List<Pole>? _cachedPoles;
  DateTime? _polesLastFetched;
  List<PoleCheckout>? _cachedHistory;
  DateTime? _historyLastFetched;

  // Cache TTL constants
  static const Duration _polesCacheTTL = Duration(minutes: 10);
  static const Duration _historyCacheTTL = Duration(hours: 1);

  // Cache validation
  bool get _isPolesCacheValid {
    if (_cachedPoles == null || _polesLastFetched == null) return false;
    return DateTime.now().difference(_polesLastFetched!) < _polesCacheTTL;
  }
}

Debug Logging

The service provides comprehensive debug logging to track cache behavior:

🚀 Returning cached poles (100 items)
📡 Fetching poles from Firestore (cache expired)
✅ Poles cached (100 items)
🗑️  Invalidating pole cache
🔄 Force refreshing all cached data

Modified Files

  1. lib/services/cached_pole_inventory_service.dart (NEW)
    • 330 lines
    • Complete caching wrapper with TTL logic
    • Automatic invalidation
    • Debug logging
  2. lib/screens/admin/pole_inventory_screen.dart
    • Changed: PoleInventoryService → CachedPoleInventoryService
    • Added: forceRefresh parameter to _loadData()
    • Added: RefreshIndicator to all three tabs
    • Updated: Refresh button to use forceRefresh: true
  3. lib/screens/admin/bulk_checkout_screen.dart
    • Changed: PoleInventoryService → CachedPoleInventoryService
    • Benefits from automatic cache invalidation on checkout/checkin
  4. lib/utils/pole_import_util.dart
    • Changed: PoleInventoryService → CachedPoleInventoryService
    • Benefits from automatic cache invalidation on pole import

Performance Impact

Read Reduction Analysis

Scenario: Admin opens pole inventory screen 10 times in one day

Operation Without Cache With Cache Savings
Screen open #1 620 reads 620 reads 0%
Screen open #2 (5 min later) 620 reads 0 reads 100%
Screen open #3 (5 min later) 620 reads 0 reads 100%
Screen open #4 (15 min later) 620 reads 100 reads* 84%
Screen open #5-10 3,720 reads Variable** ~85%
Total Daily 6,200 reads ~900 reads* 85%

* Pole cache expired (10 min), history cache still valid (1 hour)
** Depends on time between opens and cache expiration
*** Estimated average with typical usage patterns

Cost Savings

Firestore Pricing (as of 2024):

While individual savings seem small, this scales significantly with:

User Experience Benefits

  1. Faster load times - Cached data loads instantly
  2. Offline support - Works with Firestore offline persistence
  3. Reduced network usage - Less data transfer on mobile devices
  4. Battery savings - Fewer network requests
  5. Smoother UI - No loading spinners on cache hits

Usage Guidelines

For Developers

When to use force refresh:

When NOT to use force refresh:

Testing cache behavior:

// In tests, use the .test() constructor
final cachedService = CachedPoleInventoryService.test(
  poleService: mockPoleService,
);

For Users

To get latest data:

  1. Pull down on any tab (pull-to-refresh)
  2. Tap refresh icon in top-right corner
  3. Wait for cache to expire (10 minutes for poles, 1 hour for history)

Cache indicators:

Testing

Manual Testing Checklist

Expected Debug Output

📡 Fetching poles from Firestore (cache force refresh)
📡 Using real-time stream for active checkouts (no cache)
📡 Fetching checkout history from Firestore (cache force refresh)
✅ Poles cached (100 items)
✅ Checkout history cached (500 items)
🗑️  Invalidating pole cache
📡 Fetching poles from Firestore (cache expired)
✅ Poles cached (101 items)

Future Enhancements

Potential Improvements

  1. Persistent cache - Save cache to disk for app restart persistence
  2. Background refresh - Refresh cache in background when near expiration
  3. Selective invalidation - Only invalidate specific items instead of entire cache
  4. Cache statistics - Track cache hit/miss rates for analytics
  5. Configurable TTL - Allow admins to configure cache duration via settings
  6. Network-aware caching - Longer TTL on cellular, shorter on WiFi

Considerations

Troubleshooting

Issue: Data not updating

Symptoms: Changes made on one device not visible on another

Cause: Each device has independent cache

Solution:

Issue: High read count persists

Symptoms: Firestore usage hasn’t decreased after implementing caching

Cause: Active checkouts still use real-time stream (intentional)

Solution: This is expected behavior. Verify reduction in poles/history reads specifically.

Issue: Cache never expires

Symptoms: Debug console always shows cached data

Cause: User opening screen frequently (within TTL window)

Solution: This is correct behavior - working as intended!

Version History

Date Version Changes
2024-11-27 1.0.0 Initial caching implementation with TTL-based strategy

Author: GitHub Copilot
Last Updated: November 27, 2024
Status: Production Ready