672 lines
15 KiB
Markdown
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.
|
|
|