refactor: Remove generic try-catch blocks and add explicit API error handling in activity places service.
All checks were successful
Deploy Flutter to Firebase (Mac) / deploy-android (push) Successful in 3m41s

This commit is contained in:
Van Leemput Dayron
2025-12-30 16:05:04 +01:00
parent bb5a89a06d
commit 5a682bb6d7
3 changed files with 240 additions and 256 deletions

View File

@@ -174,7 +174,11 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
'Erreur recherche activités: $e', 'Erreur recherche activités: $e',
stackTrace, stackTrace,
); );
emit(const ActivityError('Impossible de rechercher les activités')); // Extraire le message d'erreur si disponible
final errorMessage = e.toString().replaceAll('Exception: ', '');
emit(
ActivityError('Impossible de rechercher les activités: $errorMessage'),
);
} }
} }

View File

@@ -37,83 +37,75 @@ class ActivityPlacesService {
int maxResults = 20, int maxResults = 20,
int offset = 0, int offset = 0,
}) async { }) async {
try { LoggerService.info(
LoggerService.info( 'ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)',
'ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)', );
// 1. Géocoder la destination
final coordinates = await _geocodeDestination(destination);
// 2. Rechercher les activités par catégorie ou toutes les catégories
List<Activity> allActivities = [];
if (category != null) {
final activities = await _searchByCategory(
coordinates['lat']!,
coordinates['lng']!,
category,
tripId,
radius,
); );
allActivities.addAll(activities);
} else {
// Rechercher dans toutes les catégories principales
final mainCategories = [
ActivityCategory.attraction,
ActivityCategory.museum,
ActivityCategory.restaurant,
ActivityCategory.culture,
ActivityCategory.nature,
];
// 1. Géocoder la destination for (final cat in mainCategories) {
final coordinates = await _geocodeDestination(destination);
// 2. Rechercher les activités par catégorie ou toutes les catégories
List<Activity> allActivities = [];
if (category != null) {
final activities = await _searchByCategory( final activities = await _searchByCategory(
coordinates['lat']!, coordinates['lat']!,
coordinates['lng']!, coordinates['lng']!,
category, cat,
tripId, tripId,
radius, radius,
); );
allActivities.addAll(activities); allActivities.addAll(activities);
} else {
// Rechercher dans toutes les catégories principales
final mainCategories = [
ActivityCategory.attraction,
ActivityCategory.museum,
ActivityCategory.restaurant,
ActivityCategory.culture,
ActivityCategory.nature,
];
for (final cat in mainCategories) {
final activities = await _searchByCategory(
coordinates['lat']!,
coordinates['lng']!,
cat,
tripId,
radius,
);
allActivities.addAll(activities);
}
} }
}
// 3. Supprimer les doublons et trier par note // 3. Supprimer les doublons et trier par note
final uniqueActivities = _removeDuplicates(allActivities); final uniqueActivities = _removeDuplicates(allActivities);
uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0)); uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0));
LoggerService.info(
'ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total',
);
// 4. Appliquer la pagination
final startIndex = offset;
final endIndex = (startIndex + maxResults).clamp(
0,
uniqueActivities.length,
);
if (startIndex >= uniqueActivities.length) {
LoggerService.info( LoggerService.info(
'ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total', 'ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})',
); );
// 4. Appliquer la pagination
final startIndex = offset;
final endIndex = (startIndex + maxResults).clamp(
0,
uniqueActivities.length,
);
if (startIndex >= uniqueActivities.length) {
LoggerService.info(
'ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})',
);
return [];
}
final paginatedResults = uniqueActivities.sublist(startIndex, endIndex);
LoggerService.info(
'ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)',
);
return paginatedResults;
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur lors de la recherche: $e',
);
_errorService.logError('activity_places_service', e);
return []; return [];
} }
final paginatedResults = uniqueActivities.sublist(startIndex, endIndex);
LoggerService.info(
'ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)',
);
return paginatedResults;
} }
/// Géocode une destination pour obtenir les coordonnées /// Géocode une destination pour obtenir les coordonnées
@@ -191,50 +183,52 @@ class ActivityPlacesService {
String tripId, String tripId,
int radius, int radius,
) async { ) async {
try { final url =
final url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng'
'?location=$lat,$lng' '&radius=$radius'
'&radius=$radius' '&type=${category.googlePlaceType}'
'&type=${category.googlePlaceType}' '&key=$_apiKey'
'&key=$_apiKey' '&language=fr';
'&language=fr';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); final data = json.decode(response.body);
if (data['status'] == 'OK') { if (data['status'] == 'OK') {
final List<Activity> activities = []; final List<Activity> activities = [];
for (final place in data['results']) { for (final place in data['results']) {
try { try {
final activity = await _convertPlaceToActivity( final activity = await _convertPlaceToActivity(
place, place,
tripId, tripId,
category, category,
); );
if (activity != null) { if (activity != null) {
activities.add(activity); activities.add(activity);
}
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
return activities;
} }
}
return []; return activities;
} catch (e) { } else if (data['status'] == 'ZERO_RESULTS') {
LoggerService.error( return [];
'ActivityPlacesService: Erreur recherche par catégorie: $e', } else {
); LoggerService.error(
return []; 'ActivityPlacesService: Erreur API Places: ${data['status']} - ${data['error_message']}',
);
throw Exception(
'API Places Error: ${data['status']} - ${data['error_message']}',
);
}
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
} }
} }
@@ -357,62 +351,62 @@ class ActivityPlacesService {
required String tripId, required String tripId,
int radius = 5000, int radius = 5000,
}) async { }) async {
try { LoggerService.info(
LoggerService.info( 'ActivityPlacesService: Recherche textuelle: $query à $destination',
'ActivityPlacesService: Recherche textuelle: $query à $destination', );
);
// Géocoder la destination // Géocoder la destination
final coordinates = await _geocodeDestination(destination); final coordinates = await _geocodeDestination(destination);
final encodedQuery = Uri.encodeComponent(query); final encodedQuery = Uri.encodeComponent(query);
final url = final url =
'https://maps.googleapis.com/maps/api/place/textsearch/json' 'https://maps.googleapis.com/maps/api/place/textsearch/json'
'?query=$encodedQuery in $destination' '?query=$encodedQuery in $destination'
'&location=${coordinates['lat']},${coordinates['lng']}' '&location=${coordinates['lat']},${coordinates['lng']}'
'&radius=$radius' '&radius=$radius'
'&key=$_apiKey' '&key=$_apiKey'
'&language=fr'; '&language=fr';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); final data = json.decode(response.body);
if (data['status'] == 'OK') { if (data['status'] == 'OK') {
final List<Activity> activities = []; final List<Activity> activities = [];
for (final place in data['results']) { for (final place in data['results']) {
try { try {
// Déterminer la catégorie basée sur les types du lieu // Déterminer la catégorie basée sur les types du lieu
final types = List<String>.from(place['types'] ?? []); final types = List<String>.from(place['types'] ?? []);
final category = _determineCategoryFromTypes(types); final category = _determineCategoryFromTypes(types);
final activity = await _convertPlaceToActivity( final activity = await _convertPlaceToActivity(
place, place,
tripId, tripId,
category, category,
); );
if (activity != null) { if (activity != null) {
activities.add(activity); activities.add(activity);
}
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
return activities;
} }
}
return []; return activities;
} catch (e) { } else if (data['status'] == 'ZERO_RESULTS') {
LoggerService.error( return [];
'ActivityPlacesService: Erreur recherche textuelle: $e', } else {
); LoggerService.error(
return []; 'ActivityPlacesService: Erreur API Places Text Search: ${data['status']}',
);
throw Exception('API Error: ${data['status']}');
}
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
} }
} }
@@ -523,70 +517,63 @@ class ActivityPlacesService {
int pageSize, int pageSize,
String? nextPageToken, String? nextPageToken,
) async { ) async {
try { String url =
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng'
'?location=$lat,$lng' '&radius=$radius'
'&radius=$radius' '&type=${category.googlePlaceType}'
'&type=${category.googlePlaceType}' '&key=$_apiKey'
'&key=$_apiKey' '&language=fr';
'&language=fr';
if (nextPageToken != null) { if (nextPageToken != null) {
url += '&pagetoken=$nextPageToken'; url += '&pagetoken=$nextPageToken';
} }
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); final data = json.decode(response.body);
if (data['status'] == 'OK') { if (data['status'] == 'OK') {
final List<Activity> activities = []; final List<Activity> activities = [];
final results = data['results'] as List? ?? []; final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats // Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList(); final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) { for (final place in limitedResults) {
try { try {
final activity = await _convertPlaceToActivity( final activity = await _convertPlaceToActivity(
place, place,
tripId, tripId,
category, category,
); );
if (activity != null) { if (activity != null) {
activities.add(activity); activities.add(activity);
}
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
'hasMoreData': data['next_page_token'] != null,
};
} }
}
return { return {
'activities': <Activity>[], 'activities': activities,
'nextPageToken': null, 'nextPageToken': data['next_page_token'],
'hasMoreData': false, 'hasMoreData': data['next_page_token'] != null,
}; };
} catch (e) { } else if (data['status'] == 'ZERO_RESULTS') {
LoggerService.error( return {
'ActivityPlacesService: Erreur recherche catégorie paginée: $e', 'activities': <Activity>[],
); 'nextPageToken': null,
return { 'hasMoreData': false,
'activities': <Activity>[], };
'nextPageToken': null, } else {
'hasMoreData': false, throw Exception('API Error: ${data['status']}');
}; }
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
} }
} }
@@ -599,75 +586,68 @@ class ActivityPlacesService {
int pageSize, int pageSize,
String? nextPageToken, String? nextPageToken,
) async { ) async {
try { // Pour toutes les catégories, on utilise une recherche plus générale
// Pour toutes les catégories, on utilise une recherche plus générale String url =
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng'
'?location=$lat,$lng' '&radius=$radius'
'&radius=$radius' '&type=tourist_attraction'
'&type=tourist_attraction' '&key=$_apiKey'
'&key=$_apiKey' '&language=fr';
'&language=fr';
if (nextPageToken != null) { if (nextPageToken != null) {
url += '&pagetoken=$nextPageToken'; url += '&pagetoken=$nextPageToken';
} }
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); final data = json.decode(response.body);
if (data['status'] == 'OK') { if (data['status'] == 'OK') {
final List<Activity> activities = []; final List<Activity> activities = [];
final results = data['results'] as List? ?? []; final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats // Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList(); final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) { for (final place in limitedResults) {
try { try {
// Déterminer la catégorie basée sur les types du lieu // Déterminer la catégorie basée sur les types du lieu
final types = List<String>.from(place['types'] ?? []); final types = List<String>.from(place['types'] ?? []);
final category = _determineCategoryFromTypes(types); final category = _determineCategoryFromTypes(types);
final activity = await _convertPlaceToActivity( final activity = await _convertPlaceToActivity(
place, place,
tripId, tripId,
category, category,
); );
if (activity != null) { if (activity != null) {
activities.add(activity); activities.add(activity);
}
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
} catch (e) {
LoggerService.error(
'ActivityPlacesService: Erreur conversion place: $e',
);
} }
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
'hasMoreData': data['next_page_token'] != null,
};
} }
}
return { return {
'activities': <Activity>[], 'activities': activities,
'nextPageToken': null, 'nextPageToken': data['next_page_token'],
'hasMoreData': false, 'hasMoreData': data['next_page_token'] != null,
}; };
} catch (e) { } else if (data['status'] == 'ZERO_RESULTS') {
LoggerService.error( return {
'ActivityPlacesService: Erreur recherche toutes catégories paginée: $e', 'activities': <Activity>[],
); 'nextPageToken': null,
return { 'hasMoreData': false,
'activities': <Activity>[], };
'nextPageToken': null, } else {
'hasMoreData': false, throw Exception('API Error: ${data['status']}');
}; }
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
} }
} }
} }

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.3+1 version: 2025.12.1+1
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2