Cache refactor complete
Successfully completed comprehensive refactoring to eliminate over-engineering and introduce reusable patterns for persistence and cache management across the Flipdare codebase.
Result: Reduced persistence architecture from 4 layers to 2 layers while introducing scalable, reusable patterns.
Service (HomeFilterService)
↓
FilterPersistenceService (redundant wrapper)
↓
AppStore
↓
SharedPreferences
Problems:
- 4 layers for simple persistence operations
- FilterPersistenceService was just a wrapper around AppStore calls
- No code reuse - each service needed custom persistence logic
- AppCacheManager had hardcoded service references - doesn’t scale
- Duplicate code in presentation and payments packages
Service (HomeFilterService/StripeOnboardingService)
↓ (uses PersistableState mixin)
PersistableState<T>
↓
AppStore
↓
SharedPreferences
Benefits:
- 2 layers with clean separation of concerns
- Reusable mixin eliminates boilerplate
- Services self-register with CacheRegistry
- Infinitely scalable without manager changes
- Shared infrastructure in services package
Location: packages/services/lib/provider/persistable_state.dart
Purpose: Provides reusable persistence logic for any Riverpod Notifier that needs AppStore persistence.
Usage:
class HomeFilterService extends _$HomeFilterService
with PersistableState<FilterState> {
@override
String get persistenceKey => AppPersistenceKey.homeFilterState.key;
@override
FilterState fromJsonString(String json) =>
FilterState.fromJson(jsonDecode(json));
@override
String toJsonString(FilterState state) =>
jsonEncode(FilterState.toJson(state));
@override
FilterState build() {
// Initialize persistence with default state
initializePersistence(defaultState: HomeFilterState());
// Register with cache registry
ref.read(cacheRegistryProvider.notifier)
.register('homeFilter', clearPersistedStateAndReset);
return currentState;
}
// Methods to update state
Future<void> updateFilters(FilterState newState) async {
await updateAndPersist(newState);
}
}
Mixin API:
initializePersistence({required T defaultState})- Initialize with default stateupdateAndPersist(T newState)- Update and persist staterefreshFromStorage()- Reload from AppStoreclearPersistedState()- Remove from AppStoreloadPersistedState()- Load from AppStore (returns Future<T?>)persistState(T state)- Persist to AppStore
Contract:
- Services must implement:
persistenceKey,fromJsonString,toJsonString - Mixin provides: All persistence operations
Location: packages/services/lib/provider/cache_registry.dart
Purpose: Decoupled registration-based cache management that scales infinitely without code changes.
How It Works:
- Services register themselves:
// In service's build() method
ref.read(cacheRegistryProvider.notifier)
.register('homeFilter', clearPersistedStateAndReset);
- Manager clears without knowing services:
class AppCacheManager extends _$AppCacheManager {
CacheRegistry get _registry => ref.read(cacheRegistryProvider.notifier);
Future<void> clearFilterCache() => _registry.clearService('homeFilter');
Future<void> clearAll() => _registry.clearAll();
}
- Infinite scalability:
- New services just register themselves
- No changes needed in AppCacheManager
- Registry manages all callbacks
API:
register(String key, Future<void> Function() cleanup)- Register cleanup callbackclearService(String key)- Clear specific serviceclearAll()- Clear all registered servicesunregister(String key)- Remove registration
services/lib/provider/
├── persistable_state.dart # Reusable persistence mixin
└── cache_registry.dart # Registration-based cache manager
Dependencies: core package (AppStore, logging)
presentation/lib/provider/
├── home/
│ └── home_filter_provider.dart # Uses PersistableState
└── app_cache_manager.dart # Uses CacheRegistry
Dependencies: services package (PersistableState, CacheRegistry)
payments/lib/provider/onboarding/
└── stripe_onboarding_provider.dart # Uses PersistableState
Dependencies: services package (PersistableState)
- Add mixin to service:
class MyService extends _$MyService
with PersistableState<MyState> {
@override
String get persistenceKey => 'myServiceState';
@override
MyState fromJsonString(String json) =>
MyState.fromJson(jsonDecode(json));
@override
String toJsonString(MyState state) =>
jsonEncode(MyState.toJson(state));
@override
MyState build() {
initializePersistence(defaultState: MyState.initial());
ref.read(cacheRegistryProvider.notifier)
.register('myService', clearPersistedStateAndReset);
return currentState;
}
}
- Use persistence methods:
Future<void> updateMyState(MyState newState) async {
await updateAndPersist(newState);
}
- That’s it! No manager changes needed.
Future<void> clearMyServiceCache() =>
_registry.clearService('myService');
presentation/lib/provider/filter/
filter_service.dart- FilterPersistenceService class (redundant wrapper)filter_service.g.dart- Generated provider
presentation/lib/provider/mixins/
persistable_state.dart- Moved to services package
presentation/lib/provider/
cache_registry.dart- Moved to services packagecache_registry.g.dart- Moved to services package
payments/lib/mixins/
persistable_state.dart- Duplicate removed (use services version)
Changes:
- Added async initialization delay:
test('should handle loading persisted state', () async {
await Future.delayed(Duration(milliseconds: 100)); // Wait for async init
final filterState = container.read(homeFilterServiceProvider);
expect(filterState.show?.active.length, equals(1));
});
- Removed FilterPersistenceService test
- Added service-based persistence test
- All tests use TestAppStore singleton pattern
Setup:
setUp(() {
AppStore.instance = TestAppStore();
container = ProviderContainer(overrides: []);
});
Ran build_runner on 3 packages to generate Riverpod provider code:
# services package
cd e:\working\flipdare\code\packages\services
dart run build_runner build --delete-conflicting-outputs
# Built in 6s, wrote 2 outputs (cache_registry.g.dart)
# presentation package
cd e:\working\flipdare\code\packages\presentation
dart run build_runner build --delete-conflicting-outputs
# Built in 32s, wrote 10 outputs (home_filter_provider.g.dart, etc.)
# payments package
cd e:\working\flipdare\code\packages\payments
dart run build_runner build --delete-conflicting-outputs
# Built in 18s, wrote 2 outputs (stripe_onboarding_provider.g.dart)
All builds successful with no errors.
✅ All compilation errors resolved
✅ Build successful across all 3 packages
✅ No errors in refactored files:
presentation/lib/provider/home/home_filter_provider.dartpresentation/lib/provider/app_cache_manager.dartpayments/lib/provider/onboarding/stripe_onboarding_provider.dartservices/lib/provider/cache_registry.dart
- ❌ 4 layers of indirection for persistence
- ❌ Redundant wrapper classes
- ❌ No code reuse
- ❌ Hardcoded cache management
- ❌ Duplicate code in multiple packages
- ✅ 2 layers with clean separation
- ✅ Reusable mixin eliminates boilerplate
- ✅ Self-registering services
- ✅ Infinitely scalable cache registry
- ✅ Shared infrastructure in services package
- ✅ Same pattern used in presentation and payments
- Add more services - Any service needing persistence can now use PersistableState mixin
- Expand CacheRegistry - Add more granular cache clearing categories
- Monitoring - Add logging/metrics to track cache operations
- Testing utilities - Create test helpers for PersistableState mixin
See:
services/lib/provider/persistable_state.dart- Mixin implementation with documentationservices/lib/provider/cache_registry.dart- Registry implementationpresentation/lib/provider/home/home_filter_provider.dart- Example usagepayments/lib/provider/onboarding/stripe_onboarding_provider.dart- Another example
Services using PersistableState mixin must use @Riverpod(keepAlive: true) to prevent auto-disposal during async initialization:
@Riverpod(keepAlive: true) // ← REQUIRED!
class HomeFilterService extends _$HomeFilterService
with PersistableState<FilterState> {
// ...
}
Without keepAlive: true, the provider may be disposed before async persistence loading completes, causing “Cannot use Ref after disposed” errors.
Also required for CacheRegistry:
@Riverpod(keepAlive: true) // ← REQUIRED!
class CacheRegistry extends _$CacheRegistry {
// ...
}
Date: 2025
Author: GitHub Copilot
Status: Complete ✅