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:
dahoud
2026-03-16 05:15:38 +00:00
parent 775729b4c3
commit 5c5ec3ad00
10 changed files with 3607 additions and 154 deletions

205
lib/core/cache/cache_service.dart vendored Normal file
View 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,
};
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:convert';
import 'cache_service.dart';
import '../utils/logger.dart';
/// Décorateur générique pour ajouter du cache à n'importe quelle méthode
/// Utilise un pattern cache-aside : vérifie le cache, sinon appelle l'API
class CachedDatasourceDecorator {
final CacheService _cacheService;
CachedDatasourceDecorator(this._cacheService);
/// Exécute une fonction avec cache
/// Si les données sont en cache et valides, les retourne
/// Sinon, appelle fetchFunction et met en cache le résultat
Future<T> withCache<T>({
required String cacheKey,
required Future<T> Function() fetchFunction,
T Function(dynamic)? deserializer,
}) async {
try {
// 1. Vérifier le cache
final cached = _cacheService.get(cacheKey);
if (cached != null) {
AppLogger.debug('✅ Cache HIT: $cacheKey');
if (deserializer != null) {
return deserializer(cached);
}
return cached as T;
}
// 2. Cache MISS - appeler l'API
AppLogger.debug('❌ Cache MISS: $cacheKey - Fetching from API');
final result = await fetchFunction();
// 3. Mettre en cache
await _cacheService.set(cacheKey, result);
return result;
} catch (e) {
AppLogger.error('Erreur dans withCache pour $cacheKey', e);
rethrow;
}
}
/// Invalide (supprime) une entrée de cache
Future<void> invalidate(String cacheKey) async {
await _cacheService.remove(cacheKey);
AppLogger.info('Cache invalidé: $cacheKey');
}
/// Invalide toutes les entrées commençant par un préfixe
Future<void> invalidatePrefix(String prefix) async {
await _cacheService.clearByPrefix(prefix);
AppLogger.info('Cache invalidé pour préfixe: $prefix');
}
}
/// Extension pour faciliter l'utilisation du cache
extension CachedCall<T> on Future<T> Function() {
/// Ajoute du cache à une fonction asynchrone
Future<T> withCache(
CachedDatasourceDecorator decorator,
String cacheKey,
) {
return decorator.withCache(
cacheKey: cacheKey,
fetchFunction: this,
);
}
}