docs(mobile): documentation complète Spec 001 + architecture
Documentation ajoutée : - ARCHITECTURE.md : Clean Architecture par feature, BLoC pattern - OPTIMISATIONS_PERFORMANCE.md : Cache multi-niveaux, pagination, lazy loading - SECURITE_PRODUCTION.md : FlutterSecureStorage, JWT, HTTPS, ProGuard - CHANGELOG.md : Historique versions - CONTRIBUTING.md : Guide contribution - README.md : Mise à jour (build, env config) Widgets partagés : - file_upload_widget.dart : Upload fichiers (photos/PDFs) Cache : - lib/core/cache/ : Système cache L1/L2 (mémoire/disque) Dependencies : - pubspec.yaml : file_picker 8.1.2, injectable, dio Spec 001 : 27/27 tâches (100%) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
205
lib/core/cache/cache_service.dart
vendored
Normal file
205
lib/core/cache/cache_service.dart
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
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', 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', 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', 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', 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', 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', 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user