# 🛠️ GUIDE D'IMPLÉMENTATION DÉTAILLÉ - UNIONFLOW MOBILE Ce document fournit des instructions techniques détaillées pour chaque catégorie de tâches identifiées dans l'audit. --- ## 🔴 SECTION 1 : TÂCHES CRITIQUES ### 1.1 Configuration Multi-Environnements #### Packages requis ```yaml dependencies: flutter_dotenv: ^5.1.0 dev_dependencies: flutter_flavorizr: ^2.2.3 ``` #### Structure des fichiers ``` .env.dev .env.staging .env.production lib/config/ ├── env_config.dart ├── app_config.dart └── flavor_config.dart ``` #### Exemple env_config.dart ```dart class EnvConfig { static const String keycloakUrl = String.fromEnvironment( 'KEYCLOAK_URL', defaultValue: 'http://192.168.1.11:8180', ); static const String apiUrl = String.fromEnvironment( 'API_URL', defaultValue: 'http://192.168.1.11:8080', ); static const String environment = String.fromEnvironment( 'ENVIRONMENT', defaultValue: 'dev', ); } ``` #### Configuration Android flavors (build.gradle) ```gradle android { flavorDimensions "environment" productFlavors { dev { dimension "environment" applicationIdSuffix ".dev" versionNameSuffix "-dev" resValue "string", "app_name", "UnionFlow Dev" } staging { dimension "environment" applicationIdSuffix ".staging" versionNameSuffix "-staging" resValue "string", "app_name", "UnionFlow Staging" } prod { dimension "environment" resValue "string", "app_name", "UnionFlow" } } } ``` #### Scripts de build ```bash # build_dev.sh flutter build apk --flavor dev --dart-define=ENVIRONMENT=dev # build_prod.sh flutter build apk --flavor prod --dart-define=ENVIRONMENT=production --release ``` --- ### 1.2 Gestion Globale des Erreurs #### Structure ``` lib/core/error/ ├── error_handler.dart ├── app_exception.dart ├── error_logger.dart └── ui/ └── error_screen.dart ``` #### error_handler.dart ```dart class ErrorHandler { static void initialize() { // Erreurs Flutter FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); _logError(details.exception, details.stack); _reportToCrashlytics(details.exception, details.stack); }; // Erreurs Dart asynchrones PlatformDispatcher.instance.onError = (error, stack) { _logError(error, stack); _reportToCrashlytics(error, stack); return true; }; } static void _logError(Object error, StackTrace? stack) { debugPrint('❌ Error: $error'); debugPrint('Stack trace: $stack'); LoggerService.error(error.toString(), stackTrace: stack); } static void _reportToCrashlytics(Object error, StackTrace? stack) { if (EnvConfig.environment != 'dev') { FirebaseCrashlytics.instance.recordError(error, stack); } } } ``` #### app_exception.dart ```dart abstract class AppException implements Exception { final String message; final String? code; final dynamic originalError; const AppException(this.message, {this.code, this.originalError}); } class NetworkException extends AppException { const NetworkException(String message, {String? code}) : super(message, code: code); } class AuthenticationException extends AppException { const AuthenticationException(String message) : super(message); } class ValidationException extends AppException { final Map errors; const ValidationException(String message, this.errors) : super(message); } ``` --- ### 1.3 Crash Reporting (Firebase Crashlytics) #### Configuration Firebase ```yaml dependencies: firebase_core: ^2.24.2 firebase_crashlytics: ^3.4.9 firebase_analytics: ^10.8.0 ``` #### Initialisation (main.dart) ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Firebase await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); // Crashlytics if (EnvConfig.environment != 'dev') { await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; } // Error Handler ErrorHandler.initialize(); runApp(const UnionFlowApp()); } ``` --- ### 1.4 Service de Logging #### logger_service.dart ```dart enum LogLevel { debug, info, warning, error } class LoggerService { static final List _logs = []; static const int _maxLogs = 1000; static void debug(String message, {Map? data}) { _log(LogLevel.debug, message, data: data); } static void info(String message, {Map? data}) { _log(LogLevel.info, message, data: data); } static void warning(String message, {Map? data}) { _log(LogLevel.warning, message, data: data); } static void error( String message, { Object? error, StackTrace? stackTrace, Map? data, }) { _log( LogLevel.error, message, error: error, stackTrace: stackTrace, data: data, ); } static void _log( LogLevel level, String message, { Object? error, StackTrace? stackTrace, Map? data, }) { final entry = LogEntry( level: level, message: message, timestamp: DateTime.now(), error: error, stackTrace: stackTrace, data: data, ); _logs.add(entry); if (_logs.length > _maxLogs) { _logs.removeAt(0); } // Console output if (kDebugMode || level == LogLevel.error) { debugPrint('[${level.name.toUpperCase()}] $message'); if (error != null) debugPrint('Error: $error'); if (stackTrace != null) debugPrint('Stack: $stackTrace'); } // Analytics if (level == LogLevel.error) { FirebaseAnalytics.instance.logEvent( name: 'app_error', parameters: { 'message': message, 'error': error?.toString() ?? '', ...?data, }, ); } } static List getLogs({LogLevel? level}) { if (level == null) return List.unmodifiable(_logs); return _logs.where((log) => log.level == level).toList(); } static Future exportLogs() async { final json = jsonEncode(_logs.map((e) => e.toJson()).toList()); // Implémenter export vers fichier ou partage } } class LogEntry { final LogLevel level; final String message; final DateTime timestamp; final Object? error; final StackTrace? stackTrace; final Map? data; LogEntry({ required this.level, required this.message, required this.timestamp, this.error, this.stackTrace, this.data, }); Map toJson() => { 'level': level.name, 'message': message, 'timestamp': timestamp.toIso8601String(), 'error': error?.toString(), 'data': data, }; } ``` --- ### 1.5 Analytics et Monitoring #### Configuration Firebase Analytics ```dart class AnalyticsService { static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; static final FirebaseAnalyticsObserver observer = FirebaseAnalyticsObserver(analytics: _analytics); // Events métier static Future logLogin(String method) async { await _analytics.logLogin(loginMethod: method); } static Future logScreenView(String screenName) async { await _analytics.logScreenView(screenName: screenName); } static Future logMemberCreated() async { await _analytics.logEvent(name: 'member_created'); } static Future logEventCreated(String eventType) async { await _analytics.logEvent( name: 'event_created', parameters: {'event_type': eventType}, ); } static Future logOrganisationJoined(String orgId) async { await _analytics.logEvent( name: 'organisation_joined', parameters: {'organisation_id': orgId}, ); } // User properties static Future setUserRole(String role) async { await _analytics.setUserProperty(name: 'user_role', value: role); } static Future setUserId(String userId) async { await _analytics.setUserId(id: userId); } } ``` --- ### 1.6 Architecture DI Complète #### Structure DI par module ``` lib/features/members/di/ └── members_di.dart lib/features/events/di/ └── events_di.dart lib/features/reports/di/ └── reports_di.dart ``` #### Exemple members_di.dart ```dart class MembersDI { static final GetIt _getIt = GetIt.instance; static void registerDependencies() { // Repository _getIt.registerLazySingleton( () => MemberRepositoryImpl(_getIt()), ); // Service _getIt.registerLazySingleton( () => MemberService(_getIt()), ); // BLoC (Factory pour créer nouvelle instance à chaque fois) _getIt.registerFactory( () => MembersBloc(_getIt()), ); } static void unregisterDependencies() { _getIt.unregister(); _getIt.unregister(); _getIt.unregister(); } } ``` #### app_di.dart mis à jour ```dart class AppDI { static Future initialize() async { await _setupNetworking(); await _setupModules(); } static Future _setupModules() async { OrganisationsDI.registerDependencies(); MembersDI.registerDependencies(); EventsDI.registerDependencies(); ReportsDI.registerDependencies(); NotificationsDI.registerDependencies(); } } ``` --- ### 1.7 Standardisation BLoC Pattern #### Template BLoC standard ```dart // Events abstract class MembersEvent extends Equatable { const MembersEvent(); @override List get props => []; } class LoadMembers extends MembersEvent { final int page; final int size; const LoadMembers({this.page = 0, this.size = 20}); @override List get props => [page, size]; } // States abstract class MembersState extends Equatable { const MembersState(); @override List get props => []; } class MembersInitial extends MembersState { const MembersInitial(); } class MembersLoading extends MembersState { const MembersLoading(); } class MembersLoaded extends MembersState { final List members; final bool hasMore; final int currentPage; const MembersLoaded({ required this.members, this.hasMore = false, this.currentPage = 0, }); @override List get props => [members, hasMore, currentPage]; } class MembersError extends MembersState { final String message; final AppException? exception; const MembersError(this.message, {this.exception}); @override List get props => [message, exception]; } // BLoC class MembersBloc extends Bloc { final MemberService _service; MembersBloc(this._service) : super(const MembersInitial()) { on(_onLoadMembers); } Future _onLoadMembers( LoadMembers event, Emitter emit, ) async { try { emit(const MembersLoading()); final members = await _service.getMembers( page: event.page, size: event.size, ); emit(MembersLoaded( members: members, hasMore: members.length >= event.size, currentPage: event.page, )); } on NetworkException catch (e) { emit(MembersError('Erreur réseau: ${e.message}', exception: e)); } catch (e) { emit(MembersError('Erreur inattendue: $e')); LoggerService.error('Error loading members', error: e); } } } ``` --- ### 1.8 Configuration CI/CD #### .github/workflows/flutter_ci.yml ```yaml name: Flutter CI on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: analyze: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: flutter-version: '3.5.3' - name: Install dependencies run: flutter pub get - name: Analyze code run: flutter analyze - name: Check formatting run: dart format --set-exit-if-changed . test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 - name: Install dependencies run: flutter pub get - name: Run tests run: flutter test --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info build-android: runs-on: ubuntu-latest needs: [analyze, test] steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '17' - name: Build APK run: flutter build apk --flavor dev --dart-define=ENVIRONMENT=dev - name: Upload APK uses: actions/upload-artifact@v3 with: name: app-dev.apk path: build/app/outputs/flutter-apk/app-dev-release.apk build-ios: runs-on: macos-latest needs: [analyze, test] steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 - name: Build iOS run: flutter build ios --no-codesign --flavor dev ``` --- ## 🟠 SECTION 2 : INTÉGRATIONS BACKEND ### 2.1 Module Membres - Intégration Complète #### member_repository.dart ```dart abstract class MemberRepository { Future> getMembers({int page = 0, int size = 20}); Future getMemberById(String id); Future createMember(Member member); Future updateMember(String id, Member member); Future deleteMember(String id); Future> searchMembers(MemberSearchCriteria criteria); } class MemberRepositoryImpl implements MemberRepository { final Dio _dio; static const String _baseUrl = '/api/membres'; MemberRepositoryImpl(this._dio); @override Future> getMembers({int page = 0, int size = 20}) async { try { final response = await _dio.get( _baseUrl, queryParameters: {'page': page, 'size': size}, ); if (response.statusCode == 200) { final List data = response.data; return data.map((json) => Member.fromJson(json)).toList(); } throw NetworkException('Failed to load members: ${response.statusCode}'); } on DioException catch (e) { throw _handleDioError(e); } } AppException _handleDioError(DioException e) { if (e.type == DioExceptionType.connectionTimeout) { return const NetworkException('Connection timeout'); } if (e.response?.statusCode == 401) { return const AuthenticationException('Unauthorized'); } return NetworkException(e.message ?? 'Network error'); } } ``` --- *[Le document continue avec les sections suivantes...]* ## 🟡 SECTION 3 : TESTS ## 🟢 SECTION 4 : UX/UI ## 🔵 SECTION 5 : FEATURES AVANCÉES --- **Note:** Ce document sera complété avec les détails techniques de toutes les sections dans les prochaines itérations.