Files
unionflow-mobile-apps/GUIDE_IMPLEMENTATION_DETAILLE.md

15 KiB

🛠️ 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

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

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)

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

# 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

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

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<String, String> errors;
  
  const ValidationException(String message, this.errors) : super(message);
}

1.3 Crash Reporting (Firebase Crashlytics)

Configuration Firebase

dependencies:
  firebase_core: ^2.24.2
  firebase_crashlytics: ^3.4.9
  firebase_analytics: ^10.8.0

Initialisation (main.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

enum LogLevel { debug, info, warning, error }

class LoggerService {
  static final List<LogEntry> _logs = [];
  static const int _maxLogs = 1000;
  
  static void debug(String message, {Map<String, dynamic>? data}) {
    _log(LogLevel.debug, message, data: data);
  }
  
  static void info(String message, {Map<String, dynamic>? data}) {
    _log(LogLevel.info, message, data: data);
  }
  
  static void warning(String message, {Map<String, dynamic>? data}) {
    _log(LogLevel.warning, message, data: data);
  }
  
  static void error(
    String message, {
    Object? error,
    StackTrace? stackTrace,
    Map<String, dynamic>? data,
  }) {
    _log(
      LogLevel.error,
      message,
      error: error,
      stackTrace: stackTrace,
      data: data,
    );
  }
  
  static void _log(
    LogLevel level,
    String message, {
    Object? error,
    StackTrace? stackTrace,
    Map<String, dynamic>? 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<LogEntry> getLogs({LogLevel? level}) {
    if (level == null) return List.unmodifiable(_logs);
    return _logs.where((log) => log.level == level).toList();
  }
  
  static Future<void> 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<String, dynamic>? data;
  
  LogEntry({
    required this.level,
    required this.message,
    required this.timestamp,
    this.error,
    this.stackTrace,
    this.data,
  });
  
  Map<String, dynamic> toJson() => {
    'level': level.name,
    'message': message,
    'timestamp': timestamp.toIso8601String(),
    'error': error?.toString(),
    'data': data,
  };
}

1.5 Analytics et Monitoring

Configuration Firebase Analytics

class AnalyticsService {
  static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
  static final FirebaseAnalyticsObserver observer = 
      FirebaseAnalyticsObserver(analytics: _analytics);
  
  // Events métier
  static Future<void> logLogin(String method) async {
    await _analytics.logLogin(loginMethod: method);
  }
  
  static Future<void> logScreenView(String screenName) async {
    await _analytics.logScreenView(screenName: screenName);
  }
  
  static Future<void> logMemberCreated() async {
    await _analytics.logEvent(name: 'member_created');
  }
  
  static Future<void> logEventCreated(String eventType) async {
    await _analytics.logEvent(
      name: 'event_created',
      parameters: {'event_type': eventType},
    );
  }
  
  static Future<void> logOrganisationJoined(String orgId) async {
    await _analytics.logEvent(
      name: 'organisation_joined',
      parameters: {'organisation_id': orgId},
    );
  }
  
  // User properties
  static Future<void> setUserRole(String role) async {
    await _analytics.setUserProperty(name: 'user_role', value: role);
  }
  
  static Future<void> 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

class MembersDI {
  static final GetIt _getIt = GetIt.instance;
  
  static void registerDependencies() {
    // Repository
    _getIt.registerLazySingleton<MemberRepository>(
      () => MemberRepositoryImpl(_getIt<Dio>()),
    );
    
    // Service
    _getIt.registerLazySingleton<MemberService>(
      () => MemberService(_getIt<MemberRepository>()),
    );
    
    // BLoC (Factory pour créer nouvelle instance à chaque fois)
    _getIt.registerFactory<MembersBloc>(
      () => MembersBloc(_getIt<MemberService>()),
    );
  }
  
  static void unregisterDependencies() {
    _getIt.unregister<MembersBloc>();
    _getIt.unregister<MemberService>();
    _getIt.unregister<MemberRepository>();
  }
}

app_di.dart mis à jour

class AppDI {
  static Future<void> initialize() async {
    await _setupNetworking();
    await _setupModules();
  }
  
  static Future<void> _setupModules() async {
    OrganisationsDI.registerDependencies();
    MembersDI.registerDependencies();
    EventsDI.registerDependencies();
    ReportsDI.registerDependencies();
    NotificationsDI.registerDependencies();
  }
}

1.7 Standardisation BLoC Pattern

Template BLoC standard

// Events
abstract class MembersEvent extends Equatable {
  const MembersEvent();
  @override
  List<Object?> get props => [];
}

class LoadMembers extends MembersEvent {
  final int page;
  final int size;
  const LoadMembers({this.page = 0, this.size = 20});
  @override
  List<Object?> get props => [page, size];
}

// States
abstract class MembersState extends Equatable {
  const MembersState();
  @override
  List<Object?> get props => [];
}

class MembersInitial extends MembersState {
  const MembersInitial();
}

class MembersLoading extends MembersState {
  const MembersLoading();
}

class MembersLoaded extends MembersState {
  final List<Member> members;
  final bool hasMore;
  final int currentPage;
  
  const MembersLoaded({
    required this.members,
    this.hasMore = false,
    this.currentPage = 0,
  });
  
  @override
  List<Object?> get props => [members, hasMore, currentPage];
}

class MembersError extends MembersState {
  final String message;
  final AppException? exception;
  
  const MembersError(this.message, {this.exception});
  
  @override
  List<Object?> get props => [message, exception];
}

// BLoC
class MembersBloc extends Bloc<MembersEvent, MembersState> {
  final MemberService _service;
  
  MembersBloc(this._service) : super(const MembersInitial()) {
    on<LoadMembers>(_onLoadMembers);
  }
  
  Future<void> _onLoadMembers(
    LoadMembers event,
    Emitter<MembersState> 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

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

abstract class MemberRepository {
  Future<List<Member>> getMembers({int page = 0, int size = 20});
  Future<Member?> getMemberById(String id);
  Future<Member> createMember(Member member);
  Future<Member> updateMember(String id, Member member);
  Future<void> deleteMember(String id);
  Future<List<Member>> searchMembers(MemberSearchCriteria criteria);
}

class MemberRepositoryImpl implements MemberRepository {
  final Dio _dio;
  static const String _baseUrl = '/api/membres';
  
  MemberRepositoryImpl(this._dio);
  
  @override
  Future<List<Member>> 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<dynamic> 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.