Skip to main content

Overview

The Flutter cache layer provides a local-first, offline-capable caching system with automatic synchronization. It uses SQLite for persistent storage on mobile devices and supports soft deletes with deferred hard deletion until after server sync.

Features

  • Generic Cache: Key-value storage with optional TTL
  • Entity Caching: Typed repositories for Wallet, Category, and Transaction
  • Offline-First: Works without network, queues changes for sync
  • Soft Deletes: Entities are marked deleted, hard deleted only after sync
  • Auto-Sync: Automatically syncs pending changes on app startup
  • LRU Eviction: Configurable cache size with automatic eviction

Architecture

lib/cache/
├── cache.dart                    # Barrel exports
├── cache_manager.dart            # Main entry point
├── database/
│   ├── cache_database.dart       # SQLite database setup
│   └── migrations/               # Schema migrations
├── models/                       # Data models
├── providers/                    # Storage implementations
├── repositories/                 # Entity repositories
└── services/                     # Sync service

Quick Start

Initialize

The cache is automatically initialized in bootstrap.dart:
// In bootstrap.dart - already configured
final cache = await CacheManager.initialize();
cache.configureSync(YourSyncApiClient());
await cache.initializeSync();

Using the Cache

// Get the cache instance
final cache = CacheManager.instance;

// Generic cache operations
await cache.put('user_settings', '{"theme": "dark"}');
final settings = await cache.get<String>('user_settings');

// With TTL
await cache.put('temp_data', 'value', ttl: Duration(hours: 1));

// Create entities
final wallet = await cache.createWallet(
  name: 'Cash',
  currency: 'USD',
  initialBalance: 1000.0,
);

final category = await cache.createCategory(
  name: 'Food',
  type: 'expense',
  color: 0xFFFF5722,
);

final transaction = await cache.createTransaction(
  walletId: wallet.id,
  categoryId: category.id,
  amount: 25.50,
  type: 'expense',
  description: 'Lunch',
);

Querying Entities

// Get all wallets
final wallets = await cache.wallets.findAll();

// Get transactions by wallet
final walletTx = await cache.transactions.findByWallet(wallet.id);

// Get transactions by date range
final monthlyTx = await cache.transactions.findByDateRange(
  DateTime.now().subtract(Duration(days: 30)),
  DateTime.now(),
);

// Get categories by type
final expenseCategories = await cache.categories.findByType('expense');

Deleting Entities

// Soft delete (marks as pending_delete, syncs to server first)
await cache.wallets.delete(wallet.id);

// Hard delete (only if already synced or forced)
await cache.wallets.delete(wallet.id, hardDelete: true);

Manual Sync

// Trigger manual sync
final result = await cache.sync();
if (result.success) {
  print('Sync completed');
} else {
  print('Sync failed: ${result.error}');
}

// Listen to sync progress
cache.syncProgress?.listen((progress) {
  print('${progress.step}: ${progress.percent}%');
});

Data Models

Wallet

FieldTypeDescription
idStringLocal UUID (primary key)
serverIdString?Server-assigned ID after sync
nameStringWallet name
currencyStringCurrency code (default: USD)
balancedoubleCurrent balance
isDeletedboolSoft delete flag
syncStatusSyncStatussync state
createdAtDateTimeCreation timestamp
updatedAtDateTimeLast update timestamp
syncedAtDateTime?Last successful sync

Category

FieldTypeDescription
idStringLocal UUID
serverIdString?Server ID after sync
nameStringCategory name
typeString’income’ or ‘expense’
colorint?Color value
iconString?Icon identifier
isDeletedboolSoft delete flag
syncStatusSyncStatusSync state

Transaction

FieldTypeDescription
idStringLocal UUID
serverIdString?Server ID after sync
walletIdStringReference to wallet
categoryIdString?Reference to category
amountdoubleTransaction amount
typeString’income’, ‘expense’, or ‘transfer’
descriptionString?Optional description
dateDateTimeTransaction date
isDeletedboolSoft delete flag
syncStatusSyncStatusSync state

Sync Status

Entities have one of four sync states:
  • synced: Entity is in sync with server
  • pendingCreate: New entity, waiting to be created on server
  • pendingUpdate: Modified entity, waiting to update on server
  • pendingDelete: Marked for deletion, waiting to delete on server

Implementing Your Sync API

Create a class implementing SyncApiClient:
class MySyncApiClient implements SyncApiClient {
  final Dio _dio;
  
  MySyncApiClient(this._dio);
  
  @override
  Future<SyncResult<Wallet>> createWallet(Wallet wallet) async {
    try {
      final response = await _dio.post('/wallets', data: {
        'name': wallet.name,
        'currency': wallet.currency,
        'balance': wallet.balance,
      });
      
      return SyncResult(
        success: true,
        serverId: response.data['id'],
        data: wallet.copyWith(serverId: response.data['id']),
      );
    } catch (e) {
      return SyncResult(success: false, error: e.toString());
    }
  }
  
  // ... implement other methods
}

Cache Configuration

// Initialize with max size (LRU eviction)
final cache = await CacheManager.initialize(maxSize: 1000);

// Or set later
cache.setMaxSize(1000);

// Clear all generic cache
await cache.clear();

Database Schema

The cache uses SQLite with these tables:
  • wallets: Wallet entities
  • categories: Category entities
  • transactions: Transaction entities
  • generic_cache: Key-value generic cache
  • pending_operations: Retry queue for failed operations

Testing

Use the provided StubSyncApiClient for testing without a backend:
final cache = await CacheManager.initialize();
cache.configureSync(StubSyncApiClient());
await cache.initializeSync();

Migration Guide

When the schema changes:
  1. Update _databaseVersion in cache_database.dart
  2. Add migration logic in _onUpgrade
  3. Test migration with existing data