feat: Implement centralized error handling with ErrorService; replace print statements with logging in services and blocs

feat: Add ErrorContent widget for displaying error messages in dialogs and bottom sheets
refactor: Update GroupBloc and GroupRepository to utilize ErrorService for error logging
refactor: Enhance user and trip services to log errors using ErrorService
refactor: Clean up debug print statements in GroupContent and related components
This commit is contained in:
Dayron
2025-10-15 11:43:21 +02:00
parent 03ed85bf98
commit 0162eb67f5
12 changed files with 422 additions and 197 deletions

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
class ErrorContent extends StatelessWidget {
final String title;
final String message;
final VoidCallback? onRetry;
final VoidCallback? onClose;
final IconData icon;
final Color? iconColor;
const ErrorContent({
super.key,
this.title = 'Une erreur est survenue',
required this.message,
this.onRetry,
this.onClose,
this.icon = Icons.error_outline,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final defaultIconColor = iconColor ?? Colors.red[400];
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton fermer en haut à droite
if (onClose != null)
Align(
alignment: Alignment.topRight,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: onClose,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
const SizedBox(height: 8),
// Icône d'erreur
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: defaultIconColor?.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: defaultIconColor,
),
),
const SizedBox(height: 24),
// Titre
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
// Message d'erreur
Text(
message,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Boutons d'action
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (onRetry != null) ...[
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
if (onClose != null) const SizedBox(width: 12),
],
if (onClose != null)
OutlinedButton.icon(
onPressed: onClose,
icon: const Icon(Icons.close),
label: const Text('Fermer'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
],
),
);
}
}
// Fonction helper pour afficher l'erreur en dialog
void showErrorDialog(
BuildContext context, {
String title = 'Une erreur est survenue',
required String message,
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
bool barrierDismissible = true,
}) {
showDialog(
context: context,
barrierDismissible: barrierDismissible,
builder: (BuildContext dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ErrorContent(
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry != null
? () {
Navigator.of(dialogContext).pop();
onRetry();
}
: null,
onClose: () => Navigator.of(dialogContext).pop(),
),
);
},
);
}
// Fonction helper pour afficher l'erreur en bottom sheet
void showErrorBottomSheet(
BuildContext context, {
String title = 'Une erreur est survenue',
required String message,
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
bool isDismissible = true,
}) {
showModalBottomSheet(
context: context,
isDismissible: isDismissible,
enableDrag: isDismissible,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext sheetContext) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ErrorContent(
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry != null
? () {
Navigator.of(sheetContext).pop();
onRetry();
}
: null,
onClose: () => Navigator.of(sheetContext).pop(),
),
),
);
},
);
}
// Fonction helper pour afficher en SnackBar (pour erreurs mineures)
void showErrorSnackBar(
BuildContext context, {
required String message,
VoidCallback? onRetry,
Duration duration = const Duration(seconds: 4),
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red[400],
duration: duration,
action: onRetry != null
? SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: onRetry,
)
: null,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/error/error_content.dart';
import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/group/group_bloc.dart';
@@ -18,55 +19,39 @@ class _GroupContentState extends State<GroupContent> {
@override
void initState() {
super.initState();
print('===== GroupContent: initState =====');
// Charger immédiatement sans attendre le prochain frame
WidgetsBinding.instance.addPostFrameCallback((_) {
print('===== PostFrameCallback déclenché =====');
_loadInitialData();
});
}
void _loadInitialData() {
print('===== _loadInitialData START =====');
try {
final userState = context.read<UserBloc>().state;
print('UserBloc state type: ${userState.runtimeType}');
print('UserBloc state: $userState');
if (userState is user_state.UserLoaded) {
final userId = userState.user.id;
print('✓ User chargé, ID: $userId');
print('>>> Envoi de LoadGroupsByUserId <<<');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
print('>>> LoadGroupsByUserId envoyé <<<');
} else {
print('✗ UserState n\'est pas UserLoaded');
throw Exception('Utilisateur non connecté');
}
} catch (e, stackTrace) {
print('===== ERREUR _loadInitialData =====');
print('Exception: $e');
print('StackTrace: $stackTrace');
} catch (e) {
_buildErrorState(e.toString(), '', true);
}
}
@override
Widget build(BuildContext context) {
print('===== GroupContent: build =====');
return BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, userState) {
print('>>> UserBloc builder - state type: ${userState.runtimeType}');
if (userState is user_state.UserLoading) {
print('État: UserLoading');
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (userState is user_state.UserError) {
print('État: UserError - ${userState.message}');
return Scaffold(
body: Center(
child: Column(
@@ -82,23 +67,15 @@ class _GroupContentState extends State<GroupContent> {
}
if (userState is! user_state.UserLoaded) {
print('État: Utilisateur non connecté');
return const Scaffold(
body: Center(child: Text('Utilisateur non connecté')),
);
}
print('✓ État: UserLoaded');
final user = userState.user;
return BlocConsumer<GroupBloc, GroupState>(
listener: (context, groupState) {
print('===== GroupBloc LISTENER =====');
print('State type: ${groupState.runtimeType}');
print('State: $groupState');
if (groupState is GroupOperationSuccess) {
print('>>> GroupOperationSuccess: ${groupState.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(groupState.message),
@@ -106,7 +83,6 @@ class _GroupContentState extends State<GroupContent> {
),
);
} else if (groupState is GroupError) {
print('>>> GroupError: ${groupState.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(groupState.message),
@@ -116,19 +92,6 @@ class _GroupContentState extends State<GroupContent> {
}
},
builder: (context, groupState) {
print('===== GroupBloc BUILDER =====');
print('State type: ${groupState.runtimeType}');
print('State: $groupState');
// TEST: Afficher le type exact
if (groupState is GroupsLoaded) {
print('✓✓✓ GroupsLoaded détecté ! ✓✓✓');
print('Nombre de groupes: ${groupState.groups.length}');
for (var i = 0; i < groupState.groups.length; i++) {
print(' Groupe $i: ${groupState.groups[i].name}');
}
}
return Scaffold(
body: SafeArea(
child: _buildContent(groupState, user.id),
@@ -141,11 +104,7 @@ class _GroupContentState extends State<GroupContent> {
}
Widget _buildContent(GroupState groupState, String userId) {
print('===== _buildContent =====');
print('State type: ${groupState.runtimeType}');
if (groupState is GroupLoading) {
print('>>> Affichage: Loading');
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -159,24 +118,18 @@ class _GroupContentState extends State<GroupContent> {
}
if (groupState is GroupError) {
print('>>> Affichage: Error');
return _buildErrorState(groupState.message, userId);
return _buildErrorState(groupState.message, userId, true);
}
if (groupState is GroupsLoaded) {
print('>>> Affichage: GroupsLoaded');
print('Groupes: ${groupState.groups.length}');
if (groupState.groups.isEmpty) {
print('>>> Affichage: Empty');
return _buildEmptyState();
}
print('>>> Affichage: Liste des groupes');
return _buildGroupsList(groupState.groups, userId);
}
print('>>> Affichage: Initial/Unknown');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -185,7 +138,6 @@ class _GroupContentState extends State<GroupContent> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
print('>>> Bouton refresh cliqué');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
},
child: const Text('Charger les groupes'),
@@ -196,12 +148,8 @@ class _GroupContentState extends State<GroupContent> {
}
Widget _buildGroupsList(List<Group> groups, String userId) {
print('===== _buildGroupsList =====');
print('Nombre de groupes à afficher: ${groups.length}');
return RefreshIndicator(
onRefresh: () async {
print('>>> Pull to refresh');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
await Future.delayed(const Duration(milliseconds: 500));
},
@@ -221,20 +169,17 @@ class _GroupContentState extends State<GroupContent> {
// IMPORTANT: Utiliser un simple Column au lieu de GridView pour tester
...groups.map((group) {
print('Création widget pour: ${group.name}');
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildSimpleGroupCard(group),
);
}).toList(),
})
],
),
);
}
Widget _buildSimpleGroupCard(Group group) {
print('===== _buildSimpleGroupCard: ${group.name} =====');
try {
final colors = [Colors.blue, Colors.purple, Colors.green, Colors.orange];
final color = colors[group.name.hashCode.abs() % colors.length];
@@ -248,8 +193,6 @@ class _GroupContentState extends State<GroupContent> {
.join(', ');
memberInfo += '\n$names';
}
print('Card créée avec succès');
return Card(
elevation: 2,
@@ -265,14 +208,11 @@ class _GroupContentState extends State<GroupContent> {
subtitle: Text(memberInfo),
trailing: const Icon(Icons.chevron_right),
onTap: () {
print('Tap sur: ${group.name}');
_openGroupChat(group);
},
),
);
} catch (e, stackTrace) {
print('ERREUR dans _buildSimpleGroupCard: $e');
print('StackTrace: $stackTrace');
} catch (e) {
return Card(
color: Colors.red[100],
child: const ListTile(
@@ -284,7 +224,6 @@ class _GroupContentState extends State<GroupContent> {
}
Widget _buildEmptyState() {
print('===== _buildEmptyState =====');
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
@@ -306,39 +245,49 @@ class _GroupContentState extends State<GroupContent> {
);
}
Widget _buildErrorState(String error, String userId) {
print('===== _buildErrorState =====');
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text('Erreur', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Text(error, textAlign: TextAlign.center, style: const TextStyle(fontSize: 12)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
print('>>> Bouton réessayer cliqué');
Widget _buildErrorState(String error, String userId, bool retry) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
if (retry) {
if (userId == '') {
showErrorDialog(
context,
title: 'Erreur utilisateur',
message: 'Utilisateur non connecté. Veuillez vous reconnecter.',
icon: Icons.error,
iconColor: Colors.red,
onRetry: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
);
} else {
showErrorDialog(
context,
title: 'Erreur de chargement',
message: error,
icon: Icons.cloud_off,
iconColor: Colors.orange,
onRetry: () {
context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
},
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
),
);
);
}
} else {
showErrorDialog(
context,
title: 'Erreur',
message: error,
icon: Icons.error,
iconColor: Colors.red,
);
}
}
});
return const Center(child: CircularProgressIndicator());
}
void _openGroupChat(Group group) {
print('===== _openGroupChat: ${group.name} =====');
print('Group ID: ${group.id}');
print('Group members: ${group.members.length}');
try {
// Afficher juste un message, pas de navigation pour l'instant
if (mounted) {
@@ -359,7 +308,7 @@ class _GroupContentState extends State<GroupContent> {
// );
} catch (e) {
print('ERREUR openGroupChat: $e');
_buildErrorState(e.toString(), '', false);
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/data/models/trip.dart';
import 'package:travel_mate/services/error_service.dart';
import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/trip/trip_bloc.dart';
@@ -19,6 +20,7 @@ class CreateTripContent extends StatefulWidget {
}
class _CreateTripContentState extends State<CreateTripContent> {
final _errorService = ErrorService();
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
@@ -528,7 +530,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
}
}
} catch (e) {
print('Erreur lors de la récupération de l\'utilisateur: $e');
_errorService.logError('Erreur lors de la récupération de l\'utilisateur $email: $e', StackTrace.current);
}
}