Files
unionflow-mobile-apps/docs/ARCHITECTURE.md
dahoud 5c5ec3ad00 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>
2026-03-16 05:15:38 +00:00

1011 lines
28 KiB
Markdown

# 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
1. [Vue d'Ensemble](#vue-densemble)
2. [Clean Architecture](#clean-architecture)
3. [BLoC Pattern](#bloc-pattern)
4. [Structure des Dossiers](#structure-des-dossiers)
5. [Couches Applicatives](#couches-applicatives)
6. [Flux de Données](#flux-de-données)
7. [Injection de Dépendances](#injection-de-dépendances)
8. [Gestion d'État](#gestion-détat)
9. [Communication Réseau](#communication-réseau)
10. [Cache Strategy](#cache-strategy)
11. [Authentification](#authentification)
12. [WebSocket Temps Réel](#websocket-temps-réel)
13. [Bonnes Pratiques](#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**:
```dart
// ✅ 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)
```dart
// 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)
```dart
// 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)
```dart
// 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)
```dart
// 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'app
- `widgets/` - 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 atomiques
- `repositories/` - 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**:
```dart
// 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, DB
- `models/` - DTOs avec serialization JSON
- `repositories/` - Implémentations des interfaces
**Règles**:
- ✅ Utilise `json_annotation` pour 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**:
```dart
// 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
```dart
// 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
```dart
// @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
```dart
// 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:
```dart
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded) {
return DashboardView(data: state.dashboard);
}
return LoadingWidget();
},
)
```
**BlocListener** - Side effects (navigation, snackbar, etc.):
```dart
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:
```dart
BlocConsumer<DashboardBloc, DashboardState>(
listener: (context, state) {
// Side effects
},
builder: (context, state) {
// UI rebuild
},
)
```
---
## Communication Réseau
### Dio Client
**Configuration** (`core/network/api_client.dart`):
```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
```dart
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):
```dart
static final Map<String, dynamic> _memoryCache = {};
```
**L2 Cache** - Disque (Persist après redémarrage):
```dart
final SharedPreferences _prefs;
```
### Cache Service avec TTL
**TTL configurables** (`core/cache/cache_service.dart`):
```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`):
```dart
final stats = await decorator.withCache(
cacheKey: 'dashboard_stats_${userId}',
fetchFunction: () => api.getDashboardStats(),
);
```
**Flux**:
1. Vérifier le cache
2. Si HIT: retourner valeur du cache
3. Si MISS: appeler l'API
4. Mettre en cache le résultat
5. 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:
```dart
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()`:
```dart
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):
```dart
@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**:
```dart
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.dart` contient seulement DashboardBloc
-`dashboard_event.dart` contient tous les events
-`dashboard_state.dart` contient 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):
```dart
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**:
```dart
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`):
```dart
Future<Either<Failure, Entity>> someMethod() async {
try {
final result = await api.call();
return Right(result);
} catch (e) {
return Left(ServerFailure());
}
}
```
**Failure Hierarchy**:
```dart
abstract class Failure extends Equatable {}
class ServerFailure extends Failure {}
class NetworkFailure extends Failure {}
class CacheFailure extends Failure {}
```
### 5. Performance
**Const constructors** partout:
```dart
const Text('Hello'),
const SizedBox(height: 16),
const EdgeInsets.all(8),
```
**ListView.builder** pour listes longues:
```dart
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
```
**Debounce** pour recherche:
```dart
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](../README.md) - Guide d'installation
- [SECURITE_PRODUCTION.md](SECURITE_PRODUCTION.md) - Mesures de sécurité
- [OPTIMISATIONS_PERFORMANCE.md](OPTIMISATIONS_PERFORMANCE.md) - Optimisations
- [CONTRIBUTING.md](../CONTRIBUTING.md) - Guide de contribution
---
*Document maintenu par l'équipe UnionFlow Development Team*