Files
unionflow-mobile-apps/lib/core/cache/cache_service.dart
dahoud b63fc46182 feat(mobile): amélioration UX NotImplementedFailure + SnackbarHelper
- NotImplementedFailure: ajout userFriendlyMessage et icon construction (blue)
- ErrorDisplayWidget: support spécial pour NotImplementedFailure (bientôt disponible)
- SnackbarHelper: classe centralisée pour messages cohérents (success, error, warning, info, notImplemented)
- budgets_list_page: remplace generic snackbar par SnackbarHelper.showNotImplemented
- conversations_page: remplace 2 TODOs par SnackbarHelper.showNotImplemented
- export_members: met à jour TODO obsolète (endpoint PDF maintenant disponible)
- cache_service: fix AppLogger.error calls (error: named param)
- cached_datasource_decorator: fix AppLogger.error call

Task #64 - Fix Snackbar Placeholders + NotImplementedFailure UX
2026-03-17 10:06:21 +00:00

206 lines
5.5 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/logger.dart';
/// Service de cache stratégique avec TTL (Time To Live)
/// pour optimiser les performances et réduire les appels API
class CacheService {
final SharedPreferences _prefs;
CacheService(this._prefs);
/// Clés de cache avec leur TTL en secondes
static const Map<String, int> _cacheTTL = {
'dashboard_stats': 300, // 5 minutes
'parametres_lcb_ft': 1800, // 30 minutes
'user_profile': 600, // 10 minutes
'organisations': 3600, // 1 heure
'notifications_count': 60, // 1 minute
};
/// Met en cache une valeur avec un TTL automatique selon la clé
Future<bool> set(String key, dynamic value) async {
try {
final cacheData = {
'value': value,
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
final jsonString = json.encode(cacheData);
final success = await _prefs.setString(key, jsonString);
if (success) {
AppLogger.debug('Cache set: $key (TTL: ${_getTTL(key)}s)');
}
return success;
} catch (e) {
AppLogger.error('Erreur lors de la mise en cache de $key', error: e);
return false;
}
}
/// Récupère une valeur depuis le cache si elle n'est pas expirée
/// Retourne null si la clé n'existe pas ou si le cache est expiré
T? get<T>(String key) {
try {
final jsonString = _prefs.getString(key);
if (jsonString == null) {
return null;
}
final cacheData = json.decode(jsonString) as Map<String, dynamic>;
final timestamp = cacheData['timestamp'] as int;
final now = DateTime.now().millisecondsSinceEpoch;
// Vérifier si le cache a expiré
final ttl = _getTTL(key) * 1000; // Convertir en millisecondes
if (now - timestamp > ttl) {
AppLogger.debug('Cache expiré: $key');
remove(key); // Nettoyer
return null;
}
final value = cacheData['value'];
AppLogger.debug('Cache hit: $key');
return value as T;
} catch (e) {
AppLogger.error('Erreur lors de la lecture du cache $key', error: e);
return null;
}
}
/// Récupère une valeur String depuis le cache
String? getString(String key) => get<String>(key);
/// Récupère une valeur Map depuis le cache
Map<String, dynamic>? getMap(String key) {
final value = get<Map<String, dynamic>>(key);
if (value == null) return null;
return Map<String, dynamic>.from(value);
}
/// Récupère une valeur List depuis le cache
List<dynamic>? getList(String key) {
final value = get<List<dynamic>>(key);
if (value == null) return null;
return List<dynamic>.from(value);
}
/// Supprime une clé du cache
Future<bool> remove(String key) async {
try {
return await _prefs.remove(key);
} catch (e) {
AppLogger.error('Erreur lors de la suppression du cache $key', error: e);
return false;
}
}
/// Nettoie toutes les clés d'un préfixe donné
Future<void> clearByPrefix(String prefix) async {
try {
final keys = _prefs.getKeys();
final keysToRemove = keys.where((k) => k.startsWith(prefix));
for (final key in keysToRemove) {
await remove(key);
}
AppLogger.info('Cache nettoyé pour préfixe: $prefix');
} catch (e) {
AppLogger.error('Erreur lors du nettoyage du cache $prefix', error: e);
}
}
/// Nettoie tout le cache
Future<bool> clearAll() async {
try {
return await _prefs.clear();
} catch (e) {
AppLogger.error('Erreur lors du nettoyage complet du cache', error: e);
return false;
}
}
/// Nettoie les caches expirés (maintenance)
Future<void> cleanupExpired() async {
try {
final keys = _prefs.getKeys();
int cleaned = 0;
for (final key in keys) {
final jsonString = _prefs.getString(key);
if (jsonString == null) continue;
try {
final cacheData = json.decode(jsonString) as Map<String, dynamic>;
final timestamp = cacheData['timestamp'] as int;
final now = DateTime.now().millisecondsSinceEpoch;
final ttl = _getTTL(key) * 1000;
if (now - timestamp > ttl) {
await remove(key);
cleaned++;
}
} catch (_) {
// Données corrompues, supprimer
await remove(key);
cleaned++;
}
}
if (cleaned > 0) {
AppLogger.info('$cleaned entrées de cache expirées nettoyées');
}
} catch (e) {
AppLogger.error('Erreur lors du nettoyage des caches expirés', error: e);
}
}
/// Récupère le TTL d'une clé en secondes
int _getTTL(String key) {
// Chercher une correspondance exacte
if (_cacheTTL.containsKey(key)) {
return _cacheTTL[key]!;
}
// Chercher par préfixe
for (final entry in _cacheTTL.entries) {
if (key.startsWith(entry.key)) {
return entry.value;
}
}
// TTL par défaut : 5 minutes
return 300;
}
/// Vérifie si une clé existe et n'est pas expirée
bool has(String key) {
return get(key) != null;
}
/// Retourne des statistiques sur le cache
Map<String, dynamic> getStats() {
final keys = _prefs.getKeys();
int total = keys.length;
int expired = 0;
int valid = 0;
for (final key in keys) {
if (get(key) == null) {
expired++;
} else {
valid++;
}
}
return {
'total': total,
'valid': valid,
'expired': expired,
};
}
}