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>
28 KiB
Architecture UnionFlow Mobile
Document de référence de l'architecture logicielle de l'application UnionFlow Mobile.
Version: 1.0.0 Date: 2026-03-15 Pattern: Clean Architecture + BLoC
Table des Matières
- Vue d'Ensemble
- Clean Architecture
- BLoC Pattern
- Structure des Dossiers
- Couches Applicatives
- Flux de Données
- Injection de Dépendances
- Gestion d'État
- Communication Réseau
- Cache Strategy
- Authentification
- WebSocket Temps Réel
- Bonnes Pratiques
Vue d'Ensemble
UnionFlow Mobile est construit selon les principes de Clean Architecture de Robert C. Martin, couplée au pattern BLoC (Business Logic Component) pour la gestion d'état.
Objectifs Architecturaux
✅ Séparation des préoccupations - Chaque couche a une responsabilité unique ✅ Testabilité - Code découplé et facilement testable ✅ Maintenabilité - Structure claire et cohérente ✅ Scalabilité - Ajout facile de nouvelles fonctionnalités ✅ Indépendance du framework - Business logic indépendante de Flutter
Clean Architecture
Les 3 Couches Principales
┌─────────────────────────────────────────────────────┐
│ PRESENTATION │ ← UI, BLoC, Pages, Widgets
│ (Flutter Widgets, BLoC, Events, States) │
├─────────────────────────────────────────────────────┤
│ DOMAIN │ ← Business Logic
│ (Entities, Use Cases, Repository Interfaces) │
├─────────────────────────────────────────────────────┤
│ DATA │ ← Data Sources
│ (Models, Repositories Impl, API, Cache, DB) │
└─────────────────────────────────────────────────────┘
Règles de Dépendance
Règle d'or: Les dépendances pointent toujours vers l'intérieur.
- ✅ Presentation → Domain → Data
- ❌ Data → Domain (INTERDIT)
- ❌ Domain → Presentation (INTERDIT)
Exemple:
// ✅ BON: Presentation dépend de Domain
class DashboardBloc {
final GetDashboardData useCase; // Use case du Domain
}
// ✅ BON: Data implémente interface du Domain
class DashboardRepositoryImpl implements DashboardRepository {
// Repository du Domain
}
// ❌ MAUVAIS: Domain dépend de Data
class GetDashboardData {
final DashboardRemoteDataSource dataSource; // Data layer - INTERDIT
}
BLoC Pattern
Principe
BLoC = Business Logic Component
Le BLoC est un pattern de gestion d'état qui:
- Sépare la logique métier de l'UI
- Utilise des Events (entrées) et des States (sorties)
- Communication via Streams (reactive programming)
Schéma de Flux
┌─────────┐ Event ┌──────┐ State ┌────────┐
│ Widget │ ──────────────────> │ BLoC │ ──────────────────> │ Widget │
└─────────┘ └──────┘ └────────┘
│ │
│ ├─> Use Cases (Domain)
│ ├─> Repository (Domain)
│ └─> Data Sources (Data)
│
└─────────────────────────────────────────────────────────┘
(BlocBuilder reconstruit l'UI)
Composants d'un BLoC
1. Events (Entrées)
// features/dashboard/presentation/bloc/dashboard_event.dart
abstract class DashboardEvent extends Equatable {}
class LoadDashboardData extends DashboardEvent {
@override
List<Object?> get props => [];
}
class RefreshDashboard extends DashboardEvent {
@override
List<Object?> get props => [];
}
2. States (Sorties)
// features/dashboard/presentation/bloc/dashboard_state.dart
abstract class DashboardState extends Equatable {}
class DashboardInitial extends DashboardState {
@override
List<Object?> get props => [];
}
class DashboardLoading extends DashboardState {
@override
List<Object?> get props => [];
}
class DashboardLoaded extends DashboardState {
final DashboardEntity dashboard;
DashboardLoaded(this.dashboard);
@override
List<Object?> get props => [dashboard];
}
class DashboardError extends DashboardState {
final String message;
DashboardError(this.message);
@override
List<Object?> get props => [message];
}
3. BLoC (Logique)
// features/dashboard/presentation/bloc/dashboard_bloc.dart
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
final GetDashboardData getDashboardData;
DashboardBloc({required this.getDashboardData}) : super(DashboardInitial()) {
on<LoadDashboardData>(_onLoadDashboard);
on<RefreshDashboard>(_onRefreshDashboard);
}
Future<void> _onLoadDashboard(
LoadDashboardData event,
Emitter<DashboardState> emit,
) async {
emit(DashboardLoading());
final result = await getDashboardData();
result.fold(
(failure) => emit(DashboardError(failure.message)),
(dashboard) => emit(DashboardLoaded(dashboard)),
);
}
Future<void> _onRefreshDashboard(
RefreshDashboard event,
Emitter<DashboardState> emit,
) async {
// Similar logic with cache invalidation
}
}
4. UI (Consommation)
// features/dashboard/presentation/pages/dashboard_page.dart
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<DashboardBloc>()..add(LoadDashboardData()),
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return CircularProgressIndicator();
} else if (state is DashboardLoaded) {
return _buildDashboard(state.dashboard);
} else if (state is DashboardError) {
return ErrorWidget(state.message);
}
return SizedBox.shrink();
},
),
);
}
}
Structure des Dossiers
lib/
├── core/ # Code transversal
│ ├── cache/
│ │ ├── cache_service.dart # Service cache TTL
│ │ └── cached_datasource_decorator.dart
│ ├── config/
│ │ └── environment.dart # Configuration ENV
│ ├── constants/
│ │ └── app_constants.dart # Constantes app
│ ├── di/
│ │ ├── injection_container.dart # Setup DI
│ │ ├── injection.dart # @module annotations
│ │ └── register_module.dart
│ ├── navigation/
│ │ ├── app_router.dart # go_router config
│ │ └── main_navigation_layout.dart
│ ├── network/
│ │ ├── api_client.dart # Dio setup
│ │ └── network_info.dart # Connectivity
│ ├── storage/
│ │ └── dashboard_cache_manager.dart
│ └── utils/
│ └── logger.dart # AppLogger central
│
├── features/ # Modules métier
│ ├── authentication/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ │ ├── keycloak_auth_service.dart
│ │ │ │ ├── keycloak_webview_auth_service.dart
│ │ │ │ └── permission_engine.dart
│ │ │ ├── models/
│ │ │ │ ├── user.dart
│ │ │ │ └── user_role.dart
│ │ │ └── repositories/
│ │ │ └── auth_repository_impl.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user_entity.dart
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository.dart
│ │ │ └── usecases/
│ │ │ ├── login_user.dart
│ │ │ ├── logout_user.dart
│ │ │ └── get_current_user.dart
│ │ └── presentation/
│ │ ├── bloc/
│ │ │ ├── auth_bloc.dart
│ │ │ ├── auth_event.dart
│ │ │ └── auth_state.dart
│ │ └── pages/
│ │ └── login_page.dart
│ │
│ ├── dashboard/ # Feature Dashboard
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ │ └── dashboard_remote_datasource.dart
│ │ │ ├── models/
│ │ │ │ ├── dashboard_stats_model.dart
│ │ │ │ └── dashboard_stats_model.g.dart
│ │ │ ├── repositories/
│ │ │ │ └── dashboard_repository_impl.dart
│ │ │ └── services/
│ │ │ ├── dashboard_offline_service.dart
│ │ │ └── dashboard_performance_monitor.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── dashboard_entity.dart
│ │ │ ├── repositories/
│ │ │ │ └── dashboard_repository.dart
│ │ │ └── usecases/
│ │ │ └── get_dashboard_data.dart
│ │ └── presentation/
│ │ ├── bloc/
│ │ │ ├── dashboard_bloc.dart
│ │ │ ├── dashboard_event.dart
│ │ │ └── dashboard_state.dart
│ │ ├── pages/
│ │ │ ├── connected_dashboard_page.dart
│ │ │ └── role_dashboards/
│ │ │ ├── super_admin_dashboard.dart
│ │ │ ├── org_admin_dashboard.dart
│ │ │ ├── active_member_dashboard.dart
│ │ │ └── ...
│ │ └── widgets/
│ │ ├── dashboard_stat_card.dart
│ │ └── dashboard_chart.dart
│ │
│ ├── members/ # Feature Membres
│ ├── contributions/ # Feature Cotisations
│ ├── events/ # Feature Événements
│ ├── solidarity/ # Feature Solidarité
│ ├── organizations/ # Feature Organisations
│ ├── notifications/ # Feature Notifications
│ ├── profile/ # Feature Profil
│ ├── reports/ # Feature Rapports
│ └── admin/ # Feature Administration
│
├── shared/ # Composants UI partagés
│ ├── design_system/
│ │ ├── components/
│ │ │ ├── buttons/
│ │ │ │ ├── uf_primary_button.dart
│ │ │ │ └── uf_secondary_button.dart
│ │ │ ├── cards/
│ │ │ │ ├── uf_card.dart
│ │ │ │ ├── uf_stat_card.dart
│ │ │ │ └── uf_metric_card.dart
│ │ │ ├── inputs/
│ │ │ └── uf_app_bar.dart
│ │ └── theme/
│ │ └── app_theme_sophisticated.dart
│ └── widgets/
│ ├── confirmation_dialog.dart
│ └── file_upload_widget.dart
│
└── main.dart # Point d'entrée
Couches Applicatives
1. Presentation Layer (UI)
Responsabilités:
- Afficher l'interface utilisateur
- Écouter les interactions utilisateur
- Envoyer des Events au BLoC
- Observer les States du BLoC
- Déclencher la navigation
Fichiers:
bloc/- Gestion d'état (Events, States, BLoC)pages/- Écrans de l'appwidgets/- Composants UI réutilisables
Règles:
- ❌ Pas de logique métier dans les widgets
- ❌ Pas d'appels directs aux repositories ou data sources
- ✅ Communication uniquement via BLoC
- ✅ Widgets stateless autant que possible
2. Domain Layer (Business Logic)
Responsabilités:
- Définir les Entities (objets métier purs)
- Définir les Use Cases (actions métier)
- Définir les Repository Interfaces (contrats)
Fichiers:
entities/- Objets métier purs (sans annotation)usecases/- Actions métier atomiquesrepositories/- Interfaces (abstractions)
Règles:
- ✅ Code 100% Dart pur (pas de dépendance Flutter)
- ✅ Facilement testable unitairement
- ✅ Indépendant des frameworks externes
- ❌ Pas de dépendance vers Data ou Presentation
Exemple Use Case:
// domain/usecases/get_dashboard_data.dart
class GetDashboardData {
final DashboardRepository repository;
GetDashboardData(this.repository);
Future<Either<Failure, DashboardEntity>> call() async {
return await repository.getDashboardData();
}
}
3. Data Layer (Data Sources)
Responsabilités:
- Implémenter les Repository Interfaces du Domain
- Gérer les sources de données (API, Cache, DB)
- Mapper les Models vers les Entities
Fichiers:
datasources/- Services API, Cache, DBmodels/- DTOs avec serialization JSONrepositories/- Implémentations des interfaces
Règles:
- ✅ Utilise
json_annotationpour serialization - ✅ Gère les erreurs réseau et parsing
- ✅ Implémente le cache strategy
- ❌ Pas d'exposition des Models hors de Data layer
Exemple Repository:
// data/repositories/dashboard_repository_impl.dart
class DashboardRepositoryImpl implements DashboardRepository {
final DashboardRemoteDataSource remoteDataSource;
final CacheService cacheService;
DashboardRepositoryImpl({
required this.remoteDataSource,
required this.cacheService,
});
@override
Future<Either<Failure, DashboardEntity>> getDashboardData() async {
try {
// 1. Check cache
final cached = cacheService.get<DashboardStatsModel>('dashboard_stats');
if (cached != null) {
return Right(cached.toEntity());
}
// 2. Fetch from API
final model = await remoteDataSource.getDashboardStats();
// 3. Cache result
await cacheService.set('dashboard_stats', model);
// 4. Map to Entity
return Right(model.toEntity());
} on DioException catch (e) {
return Left(NetworkFailure(e.message ?? 'Network error'));
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}
Flux de Données
Flux Complet (Exemple Dashboard)
1. USER ACTION
↓
2. Widget.onPressed → context.read<DashboardBloc>().add(LoadDashboardData())
↓
3. DashboardBloc reçoit l'event
↓
4. Bloc appelle Use Case: getDashboardData()
↓
5. Use Case appelle Repository: repository.getDashboardData()
↓
6. Repository vérifie le cache (CacheService)
├─> Cache HIT: retourne Entity depuis cache
└─> Cache MISS: appelle RemoteDataSource
↓
7. RemoteDataSource fait l'appel API (Dio)
↓
8. API retourne JSON
↓
9. JSON désérialisé en Model (@JsonSerializable)
↓
10. Model mappé en Entity (model.toEntity())
↓
11. Repository met en cache (cacheService.set)
↓
12. Repository retourne Either<Failure, Entity>
↓
13. Use Case retourne le résultat au Bloc
↓
14. Bloc émet un nouveau State: DashboardLoaded(entity)
↓
15. BlocBuilder reconstruit l'UI avec les nouvelles données
Injection de Dépendances
Get It + Injectable
// core/di/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection_container.config.dart';
final getIt = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async {
await getIt.init();
}
Annotations
// @singleton - Instance unique partagée
@singleton
class CacheService { ... }
// @lazySingleton - Instance unique créée à la demande
@lazySingleton
class KeycloakAuthService { ... }
// @injectable - Nouvelle instance à chaque injection
@injectable
class DashboardBloc {
final GetDashboardData getDashboardData;
DashboardBloc({required this.getDashboardData});
}
Enregistrement Manuel
// core/di/register_module.dart
@module
abstract class RegisterModule {
@lazySingleton
Dio get dio => Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
),
);
@lazySingleton
FlutterSecureStorage get secureStorage => const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
);
@preResolve
Future<SharedPreferences> get sharedPreferences => SharedPreferences.getInstance();
}
Gestion d'État
BLoC Lifecycle
┌─────────────┐
│ Initial │ ← État initial
└──────┬──────┘
│
├──> Event reçu
│
┌──────▼──────┐
│ Loading │ ← Chargement en cours
└──────┬──────┘
│
├──> Succès
│
┌──────▼──────┐
│ Loaded │ ← Données chargées
└──────┬──────┘
│
├──> Erreur
│
┌──────▼──────┐
│ Error │ ← Erreur survenue
└─────────────┘
BlocListener vs BlocBuilder
BlocBuilder - Reconstruction UI:
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded) {
return DashboardView(data: state.dashboard);
}
return LoadingWidget();
},
)
BlocListener - Side effects (navigation, snackbar, etc.):
BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
context.go('/dashboard');
} else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: LoginForm(),
)
BlocConsumer - Combinaison des deux:
BlocConsumer<DashboardBloc, DashboardState>(
listener: (context, state) {
// Side effects
},
builder: (context, state) {
// UI rebuild
},
)
Communication Réseau
Dio Client
Configuration (core/network/api_client.dart):
final dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// Interceptor pour ajouter le token JWT
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await getIt<KeycloakAuthService>().getValidToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
));
// Interceptor pour gérer les erreurs 401 (token expiré)
dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final newToken = await getIt<KeycloakAuthService>().refreshToken();
if (newToken != null) {
// Retry request with new token
error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
return handler.resolve(await dio.fetch(error.requestOptions));
}
}
return handler.next(error);
},
));
Error Handling
try {
final response = await dio.get('/api/dashboard/stats');
return DashboardStatsModel.fromJson(response.data);
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout) {
throw NetworkException('Connection timeout');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw NetworkException('Receive timeout');
} else if (e.response?.statusCode == 400) {
throw ValidationException(e.response?.data['message'] ?? 'Validation error');
} else if (e.response?.statusCode == 500) {
throw ServerException('Server error');
} else {
throw NetworkException('Unknown network error');
}
}
Cache Strategy
Multi-Level Cache (L1 + L2)
L1 Cache - Mémoire (Instant):
static final Map<String, dynamic> _memoryCache = {};
L2 Cache - Disque (Persist après redémarrage):
final SharedPreferences _prefs;
Cache Service avec TTL
TTL configurables (core/cache/cache_service.dart):
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
};
Cache-Aside Pattern
Decorator (core/cache/cached_datasource_decorator.dart):
final stats = await decorator.withCache(
cacheKey: 'dashboard_stats_${userId}',
fetchFunction: () => api.getDashboardStats(),
);
Flux:
- Vérifier le cache
- Si HIT: retourner valeur du cache
- Si MISS: appeler l'API
- Mettre en cache le résultat
- Retourner le résultat
Authentification
OAuth 2.0 / OIDC avec Keycloak
Flux:
1. User clique "Se connecter"
↓
2. KeycloakAuthService.login(username, password)
↓
3. POST /realms/unionflow/protocol/openid-connect/token
↓
4. Keycloak retourne: access_token, refresh_token, id_token
↓
5. Tokens stockés dans FlutterSecureStorage (chiffrés)
↓
6. JWT décodé pour extraire user info + roles
↓
7. User Entity créé avec primaryRole (RBAC)
↓
8. AuthBloc émet AuthSuccess(user)
↓
9. Navigation vers Dashboard
Refresh Token Automatique
Verrouillage global pour éviter appels concurrents:
static Future<String?>? _refreshFuture;
Future<String?> refreshToken() async {
if (_refreshFuture != null) {
return await _refreshFuture;
}
_refreshFuture = _performRefresh();
try {
return await _refreshFuture;
} finally {
_refreshFuture = null;
}
}
Auto-refresh dans getCurrentUser():
if (JwtDecoder.isExpired(token)) {
token = await refreshToken();
if (token == null) return null;
}
WebSocket Temps Réel
Architecture Event-Driven
Backend (Kafka + WebSocket):
- Kafka Topics:
unionflow.dashboard.stats,unionflow.notifications.user, etc. - Consommateur Kafka émet events sur WebSocket (
/ws/dashboard)
Mobile (WebSocket Client):
@singleton
class WebSocketService {
late WebSocketChannel _channel;
final _eventController = StreamController<dynamic>.broadcast();
Stream<dynamic> get eventStream => _eventController.stream;
void connect() {
_channel = WebSocketChannel.connect(
Uri.parse(AppConfig.wsDashboardUrl),
);
_channel.stream.listen((message) {
final event = jsonDecode(message);
_eventController.add(event);
});
}
void disconnect() {
_channel.sink.close();
_eventController.close();
}
}
Intégration BLoC:
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
final WebSocketService webSocketService;
StreamSubscription? _wsSubscription;
DashboardBloc({required this.webSocketService}) : super(DashboardInitial()) {
_wsSubscription = webSocketService.eventStream.listen((event) {
if (event['type'] == 'DASHBOARD_STATS_UPDATE') {
add(RefreshDashboardFromWebSocket(event['data']));
}
});
}
@override
Future<void> close() {
_wsSubscription?.cancel();
return super.close();
}
}
Bonnes Pratiques
1. Naming Conventions
Fichiers:
- snake_case:
dashboard_bloc.dart,user_repository.dart - Suffixes:
_bloc.dart,_event.dart,_state.dart,_page.dart,_widget.dart
Classes:
- PascalCase:
DashboardBloc,DashboardEvent,DashboardState - Suffixes:
Bloc,Event,State,Page,Widget,Repository,UseCase
Variables/Fonctions:
- camelCase:
getDashboardData,currentUser,isLoading
2. Code Organization
Un fichier = Une responsabilité:
- ✅
dashboard_bloc.dartcontient seulement DashboardBloc - ✅
dashboard_event.dartcontient tous les events - ✅
dashboard_state.dartcontient tous les states
Grouping:
- Regrouper par feature (vertical slicing)
- Pas de dossiers
utils/fourre-tout - Chaque feature = mini-application autonome
3. Testing
Tests Unitaires (Domain):
test('GetDashboardData should return DashboardEntity on success', () async {
when(mockRepository.getDashboardData()).thenAnswer(
(_) async => Right(tDashboardEntity),
);
final result = await useCase();
expect(result, Right(tDashboardEntity));
verify(mockRepository.getDashboardData());
});
Tests BLoC:
blocTest<DashboardBloc, DashboardState>(
'emits [Loading, Loaded] when LoadDashboardData succeeds',
build: () => DashboardBloc(getDashboardData: mockGetDashboardData),
act: (bloc) => bloc.add(LoadDashboardData()),
expect: () => [
DashboardLoading(),
DashboardLoaded(tDashboard),
],
);
4. Error Handling
Utiliser Either<L, R> (package dartz):
Future<Either<Failure, Entity>> someMethod() async {
try {
final result = await api.call();
return Right(result);
} catch (e) {
return Left(ServerFailure());
}
}
Failure Hierarchy:
abstract class Failure extends Equatable {}
class ServerFailure extends Failure {}
class NetworkFailure extends Failure {}
class CacheFailure extends Failure {}
5. Performance
✅ Const constructors partout:
const Text('Hello'),
const SizedBox(height: 16),
const EdgeInsets.all(8),
✅ ListView.builder pour listes longues:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
✅ Debounce pour recherche:
Timer? _debounce;
void _onSearchChanged(String query) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
// Call API
});
}
Conclusion
L'architecture UnionFlow Mobile est:
✅ Modulaire - Ajout facile de nouvelles features ✅ Testable - Code découplé et injectable ✅ Maintenable - Structure claire et cohérente ✅ Scalable - Supporte la croissance de l'application ✅ Performante - Cache multi-niveaux, lazy loading ✅ Sécurisée - Authentification robuste, storage chiffré
Documentation associée:
- README.md - Guide d'installation
- SECURITE_PRODUCTION.md - Mesures de sécurité
- OPTIMISATIONS_PERFORMANCE.md - Optimisations
- CONTRIBUTING.md - Guide de contribution
Document maintenu par l'équipe UnionFlow Development Team