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

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

  1. Vue d'Ensemble
  2. Clean Architecture
  3. BLoC Pattern
  4. Structure des Dossiers
  5. Couches Applicatives
  6. Flux de Données
  7. Injection de Dépendances
  8. Gestion d'État
  9. Communication Réseau
  10. Cache Strategy
  11. Authentification
  12. WebSocket Temps Réel
  13. 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'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:

// 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:

// 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:

  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:

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.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):

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:


Document maintenu par l'équipe UnionFlow Development Team