Clean project: remove test files, debug logs, and add documentation
This commit is contained in:
671
GUIDE_IMPLEMENTATION_DETAILLE.md
Normal file
671
GUIDE_IMPLEMENTATION_DETAILLE.md
Normal file
@@ -0,0 +1,671 @@
|
||||
# 🛠️ 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.
|
||||
|
||||
Reference in New Issue
Block a user