REST API et HTTP en Flutter
Introduction aux API REST
Une API REST (Representational State Transfer) est une architecture permettant à des applications de communiquer via le protocole HTTP. Les APIs REST utilisent des méthodes HTTP standard pour effectuer des opérations CRUD (Create, Read, Update, Delete).
Méthodes HTTP principales
| Méthode | Opération | Utilisation |
|---|---|---|
| GET | Lecture | Récupérer des données |
| POST | Création | Créer une nouvelle ressource |
| PUT | Mise à jour complète | Remplacer une ressource |
| PATCH | Mise à jour partielle | Modifier une partie d'une ressource |
| DELETE | Suppression | Supprimer une ressource |
Codes de statut HTTP
| Code | Signification | Exemple |
|---|---|---|
| 200 | OK | Requête réussie |
| 201 | Created | Ressource créée avec succès |
| 204 | No Content | Suppression réussie |
| 400 | Bad Request | Données invalides |
| 401 | Unauthorized | Non authentifié |
| 403 | Forbidden | Accès refusé |
| 404 | Not Found | Ressource introuvable |
| 500 | Internal Server Error | Erreur serveur |
Le package http
Le package http est la bibliothèque standard pour effectuer des requêtes HTTP en Flutter.
Installation
Ajoutez la dépendance dans pubspec.yaml :
dependencies:
http: ^1.1.0
Puis installez :
flutter pub get
Import
import 'package:http/http.dart' as http;
import 'dart:convert'; // Pour JSON
Requêtes GET
Récupérer des données simples
Future<String> recupererDonnees() async {
final url = Uri.parse('https://api.example.com/data');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Erreur ${response.statusCode}: ${response.reasonPhrase}');
}
} catch (e) {
throw Exception('Erreur réseau: $e');
}
}
Avec en-têtes personnalisés
Future<String> recupererAvecHeaders() async {
final url = Uri.parse('https://api.example.com/data');
final headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
};
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Erreur ${response.statusCode}');
}
}
Avec paramètres de requête
Future<String> rechercherProduits(String query, int page) async {
final url = Uri.parse('https://api.example.com/products').replace(
queryParameters: {
'q': query,
'page': page.toString(),
'limit': '20',
},
);
// URL générée: https://api.example.com/products?q=flutter&page=1&limit=20
final response = await http.get(url);
return response.body;
}
Parsing JSON
JSON vers objet Dart
import 'dart:convert';
class Product {
final int id;
final String title;
final double price;
final String? description;
Product({
required this.id,
required this.title,
required this.price,
this.description,
});
// Constructeur factory pour créer un Product depuis JSON
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
title: json['title'] as String,
price: (json['price'] as num).toDouble(),
description: json['description'] as String?,
);
}
// Convertir un Product en JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'price': price,
'description': description,
};
}
}
// Récupérer un produit
Future<Product> recupererProduit(int id) async {
final url = Uri.parse('https://api.example.com/products/$id');
final response = await http.get(url);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Product.fromJson(json);
} else {
throw Exception('Produit non trouvé');
}
}
// Récupérer une liste de produits
Future<List<Product>> recupererProduits() async {
final url = Uri.parse('https://api.example.com/products');
final response = await http.get(url);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body) as List;
return jsonList.map((json) => Product.fromJson(json as Map<String, dynamic>)).toList();
} else {
throw Exception('Erreur lors de la récupération');
}
}
Requêtes POST
Créer une ressource
Future<Product> creerProduit(Product product) async {
final url = Uri.parse('https://api.example.com/products');
try {
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(product.toJson()),
);
if (response.statusCode == 201) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Product.fromJson(json);
} else {
throw Exception('Erreur lors de la création: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur réseau: $e');
}
}
// Utilisation
void main() async {
final nouveauProduit = Product(
id: 0, // Sera assigné par le serveur
title: 'Flutter Book',
price: 29.99,
description: 'Un excellent livre sur Flutter',
);
try {
final produitCree = await creerProduit(nouveauProduit);
print('Produit créé avec l\'ID: ${produitCree.id}');
} catch (e) {
print('Erreur: $e');
}
}
Requêtes PUT et PATCH
Mise à jour complète (PUT)
Future<Product> mettreAJourProduit(int id, Product product) async {
final url = Uri.parse('https://api.example.com/products/$id');
final response = await http.put(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(product.toJson()),
);
if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la mise à jour');
}
}
Mise à jour partielle (PATCH)
Future<Product> modifierPrix(int id, double nouveauPrix) async {
final url = Uri.parse('https://api.example.com/products/$id');
final response = await http.patch(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'price': nouveauPrix}),
);
if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la modification');
}
}
Requête DELETE
Future<void> supprimerProduit(int id) async {
final url = Uri.parse('https://api.example.com/products/$id');
final response = await http.delete(url);
if (response.statusCode == 204 || response.statusCode == 200) {
print('Produit supprimé avec succès');
} else {
throw Exception('Erreur lors de la suppression');
}
}
Gestion avancée des erreurs
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException(this.statusCode, this.message);
String toString() => 'ApiException($statusCode): $message';
}
class ApiService {
static const baseUrl = 'https://api.example.com';
Future<List<Product>> recupererProduits() async {
final url = Uri.parse('$baseUrl/products');
try {
final response = await http.get(url).timeout(
Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('La requête a pris trop de temps');
},
);
return _handleResponse<List<Product>>(
response,
(json) {
final List<dynamic> list = json as List;
return list.map((item) => Product.fromJson(item as Map<String, dynamic>)).toList();
},
);
} on SocketException {
throw ApiException(0, 'Pas de connexion internet');
} on TimeoutException {
throw ApiException(0, 'Timeout de la requête');
} catch (e) {
throw ApiException(0, 'Erreur inconnue: $e');
}
}
T _handleResponse<T>(
http.Response response,
T Function(dynamic) parser,
) {
switch (response.statusCode) {
case 200:
case 201:
return parser(jsonDecode(response.body));
case 400:
throw ApiException(400, 'Requête invalide');
case 401:
throw ApiException(401, 'Non authentifié');
case 403:
throw ApiException(403, 'Accès refusé');
case 404:
throw ApiException(404, 'Ressource non trouvée');
case 500:
throw ApiException(500, 'Erreur serveur');
default:
throw ApiException(
response.statusCode,
'Erreur ${response.statusCode}: ${response.reasonPhrase}',
);
}
}
}
Client HTTP réutilisable
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:convert';
class HttpClient {
final String baseUrl;
final Map<String, String> defaultHeaders;
final Duration timeout;
HttpClient({
required this.baseUrl,
this.defaultHeaders = const {},
this.timeout = const Duration(seconds: 10),
});
// GET
Future<T> get<T>(
String endpoint, {
Map<String, String>? headers,
Map<String, String>? queryParams,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint, queryParams);
final response = await http.get(
uri,
headers: {...defaultHeaders, ...?headers},
).timeout(timeout);
return _handleResponse(response, parser);
}
// POST
Future<T> post<T>(
String endpoint, {
required dynamic body,
Map<String, String>? headers,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint);
final response = await http.post(
uri,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
...?headers,
},
body: jsonEncode(body),
).timeout(timeout);
return _handleResponse(response, parser);
}
// PUT
Future<T> put<T>(
String endpoint, {
required dynamic body,
Map<String, String>? headers,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint);
final response = await http.put(
uri,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
...?headers,
},
body: jsonEncode(body),
).timeout(timeout);
return _handleResponse(response, parser);
}
// DELETE
Future<void> delete(
String endpoint, {
Map<String, String>? headers,
}) async {
final uri = _buildUri(endpoint);
final response = await http.delete(
uri,
headers: {...defaultHeaders, ...?headers},
).timeout(timeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw HttpException('Erreur ${response.statusCode}');
}
}
Uri _buildUri(String endpoint, [Map<String, String>? queryParams]) {
final path = baseUrl.endsWith('/') ? baseUrl + endpoint : '$baseUrl/$endpoint';
return Uri.parse(path).replace(queryParameters: queryParams);
}
T _handleResponse<T>(http.Response response, T Function(dynamic)? parser) {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (parser != null && response.body.isNotEmpty) {
return parser(jsonDecode(response.body));
}
return response.body as T;
} else {
throw HttpException('Erreur ${response.statusCode}: ${response.reasonPhrase}');
}
}
}
// Utilisation
final client = HttpClient(
baseUrl: 'https://api.example.com',
defaultHeaders: {
'Authorization': 'Bearer YOUR_TOKEN',
},
);
Future<List<Product>> recupererProduits() async {
return await client.get<List<Product>>(
'products',
parser: (json) {
final list = json as List;
return list.map((item) => Product.fromJson(item as Map<String, dynamic>)).toList();
},
);
}
Bonnes pratiques
- Gérer tous les cas d'erreur : Réseau, timeout, codes HTTP, parsing JSON
- Utiliser des timeouts : Éviter les attentes infinies
- Typer les réponses : Créer des classes modèles avec fromJson/toJson
- Centraliser les requêtes : Créer un service API réutilisable
- Logger les erreurs : Aider au débogage en production
- Utiliser des constantes : Pour les URLs et endpoints
- Tester avec des API publiques : JSONPlaceholder, Fake Store API
- Respecter les limites de taux : Rate limiting des APIs
- Cacher les données : Éviter les requêtes inutiles
- Authentification sécurisée : Ne pas stocker les tokens en clair
APIs publiques pour tester
- JSONPlaceholder : https://jsonplaceholder.typicode.com
- Fake Store API : https://fakestoreapi.com
- REST Countries : https://restcountries.com
- OpenWeatherMap : https://openweathermap.org/api
- TheCatAPI : https://thecatapi.com