Files
unionflow-server-impl-quarkus/GUIDE_IMPLEMENTATION_DETAILLE.md

672 lines
15 KiB
Markdown

# 🛠️ 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<String, String> 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<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
```dart
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
```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
```dart
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
```dart
// 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
```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<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.