Reseau
Introduction
Nylo Website rend le reseau simple. Vous definissez les points d'acces API dans des classes de service qui etendent NyApiService, puis vous les appelez depuis vos pages. Le framework gere le decodage JSON, la gestion des erreurs, la mise en cache et la conversion automatique des reponses en classes de modeles (appelee "morphing").
Vos services API se trouvent dans lib/app/networking/. Un nouveau projet inclut un ApiService par defaut :
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext})
: super(
buildContext,
decoders: modelDecoders,
);
@override
String get baseUrl => getEnv('API_BASE_URL');
@override
Map<Type, Interceptor> get interceptors => {
...super.interceptors,
};
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
);
}
}
Il existe trois facons d'effectuer des requetes HTTP :
| Approche | Retourne | Ideal pour |
|---|---|---|
Methodes pratiques (get, post, etc.) |
T? |
Operations CRUD simples |
network() |
T? |
Requetes necessitant mise en cache, relances ou en-tetes personnalises |
networkResponse() |
NyResponse<T> |
Quand vous avez besoin des codes de statut, en-tetes ou details d'erreur |
En arriere-plan, Nylo Website utilise Dio, un client HTTP puissant.
Methodes pratiques
NyApiService fournit des methodes raccourcies pour les operations HTTP courantes. Elles appellent network() en interne.
Requete GET
Future<User?> fetchUser(int id) async {
return await get<User>(
"/users/$id",
queryParameters: {"include": "profile"},
);
}
Requete POST
Future<User?> createUser(Map<String, dynamic> data) async {
return await post<User>("/users", data: data);
}
Requete PUT
Future<User?> updateUser(int id, Map<String, dynamic> data) async {
return await put<User>("/users/$id", data: data);
}
Requete DELETE
Future<bool?> deleteUser(int id) async {
return await delete<bool>("/users/$id");
}
Requete PATCH
Future<User?> patchUser(int id, Map<String, dynamic> data) async {
return await patch<User>("/users/$id", data: data);
}
Requete HEAD
Utilisez HEAD pour verifier l'existence d'une ressource ou obtenir les en-tetes sans telecharger le corps :
Future<bool> checkResourceExists(String url) async {
Response response = await head(url);
return response.statusCode == 200;
}
Helper Network
La methode network vous donne plus de controle que les methodes pratiques. Elle retourne directement les donnees transformees (T?).
class ApiService extends NyApiService {
...
Future<User?> fetchUser(int id) async {
return await network<User>(
request: (request) => request.get("/users/$id"),
);
}
Future<List<User>?> fetchUsers() async {
return await network<List<User>>(
request: (request) => request.get("/users"),
);
}
Future<User?> createUser(Map<String, dynamic> data) async {
return await network<User>(
request: (request) => request.post("/users", data: data),
);
}
}
Le callback request recoit une instance Dio avec votre URL de base et vos intercepteurs deja configures.
Parametres de network
| Parametre | Type | Description |
|---|---|---|
request |
Function(Dio) |
La requete HTTP a effectuer (requis) |
bearerToken |
String? |
Jeton Bearer pour cette requete |
baseUrl |
String? |
Remplacer l'URL de base du service |
headers |
Map<String, dynamic>? |
En-tetes supplementaires |
retry |
int? |
Nombre de tentatives de relance |
retryDelay |
Duration? |
Delai entre les relances |
retryIf |
bool Function(DioException)? |
Condition pour la relance |
connectionTimeout |
Duration? |
Delai de connexion |
receiveTimeout |
Duration? |
Delai de reception |
sendTimeout |
Duration? |
Delai d'envoi |
cacheKey |
String? |
Cle de cache |
cacheDuration |
Duration? |
Duree du cache |
cachePolicy |
CachePolicy? |
Strategie de cache |
checkConnectivity |
bool? |
Verifier la connectivite avant la requete |
handleSuccess |
Function(NyResponse<T>)? |
Callback de succes |
handleFailure |
Function(NyResponse<T>)? |
Callback d'echec |
Helper networkResponse
Utilisez networkResponse lorsque vous avez besoin d'acceder a la reponse complete — codes de statut, en-tetes, messages d'erreur — pas seulement les donnees. Il retourne un NyResponse<T> au lieu de T?.
Utilisez networkResponse lorsque vous devez :
- Verifier les codes de statut HTTP pour un traitement specifique
- Acceder aux en-tetes de la reponse
- Obtenir des messages d'erreur detailles pour le retour utilisateur
- Implementer une logique de gestion d'erreur personnalisee
Future<NyResponse<User>> fetchUser(int id) async {
return await networkResponse<User>(
request: (request) => request.get("/users/$id"),
);
}
Puis utilisez la reponse dans votre page :
NyResponse<User> response = await _apiService.fetchUser(1);
if (response.isSuccessful) {
User? user = response.data;
print('Status: ${response.statusCode}');
} else {
print('Error: ${response.errorMessage}');
print('Status: ${response.statusCode}');
}
network vs networkResponse
// network() — returns the data directly
User? user = await network<User>(
request: (request) => request.get("/users/1"),
);
// networkResponse() — returns the full response
NyResponse<User> response = await networkResponse<User>(
request: (request) => request.get("/users/1"),
);
User? user = response.data;
int? status = response.statusCode;
Les deux methodes acceptent les memes parametres. Choisissez networkResponse lorsque vous devez inspecter la reponse au-dela des donnees seules.
NyResponse
NyResponse<T> encapsule la reponse Dio avec les donnees transformees et des aides de statut.
Proprietes
| Propriete | Type | Description |
|---|---|---|
response |
Response? |
Reponse Dio originale |
data |
T? |
Donnees transformees/decodees |
rawData |
dynamic |
Donnees brutes de la reponse |
headers |
Headers? |
En-tetes de la reponse |
statusCode |
int? |
Code de statut HTTP |
statusMessage |
String? |
Message de statut HTTP |
contentType |
String? |
Type de contenu depuis les en-tetes |
errorMessage |
String? |
Message d'erreur extrait |
Verifications de statut
| Getter | Description |
|---|---|
isSuccessful |
Statut 200-299 |
isClientError |
Statut 400-499 |
isServerError |
Statut 500-599 |
isRedirect |
Statut 300-399 |
hasData |
Les donnees ne sont pas null |
isUnauthorized |
Statut 401 |
isForbidden |
Statut 403 |
isNotFound |
Statut 404 |
isTimeout |
Statut 408 |
isConflict |
Statut 409 |
isRateLimited |
Statut 429 |
Aides pour les donnees
NyResponse<User> response = await apiService.fetchUser(1);
// Get data or throw if null
User user = response.dataOrThrow('User not found');
// Get data or use a fallback
User user = response.dataOr(User.guest());
// Run callback only if successful
String? greeting = response.ifSuccessful((user) => 'Hello ${user.name}');
// Pattern match on success/failure
String result = response.when(
success: (user) => 'Welcome, ${user.name}!',
failure: (response) => 'Error: ${response.statusMessage}',
);
// Get a specific header
String? authHeader = response.getHeader('Authorization');
Options de base
Configurez les options Dio par defaut pour votre service API en utilisant le parametre baseOptions :
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
baseOptions: (BaseOptions baseOptions) {
return baseOptions
..connectTimeout = Duration(seconds: 5)
..sendTimeout = Duration(seconds: 5)
..receiveTimeout = Duration(seconds: 5);
},
);
...
}
Vous pouvez egalement configurer les options dynamiquement sur une instance :
apiService.setConnectTimeout(Duration(seconds: 10));
apiService.setReceiveTimeout(Duration(seconds: 30));
apiService.setSendTimeout(Duration(seconds: 10));
apiService.setContentType('application/json');
Cliquez ici pour voir toutes les options de base que vous pouvez definir.
Ajouter des en-tetes
En-tetes par requete
Future fetchWithHeaders() async => await network(
request: (request) => request.get("/test"),
headers: {
"Authorization": "Bearer aToken123",
"Device": "iPhone"
}
);
Jeton Bearer
Future fetchUser() async => await network(
request: (request) => request.get("/user"),
bearerToken: "hello-world-123",
);
En-tetes au niveau du service
apiService.setHeaders({"X-Custom-Header": "value"});
apiService.setBearerToken("my-token");
Extension RequestHeaders
Le type RequestHeaders (un typedef Map<String, dynamic>) fournit des methodes d'aide :
@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
String? token = Auth.data(field: 'token');
if (token != null) {
headers.addBearerToken(token);
}
headers.addHeader('X-App-Version', '1.0.0');
return headers;
}
| Methode | Description |
|---|---|
addBearerToken(token) |
Definir l'en-tete Authorization: Bearer |
getBearerToken() |
Lire le jeton Bearer depuis les en-tetes |
addHeader(key, value) |
Ajouter un en-tete personnalise |
hasHeader(key) |
Verifier si un en-tete existe |
Envoi de fichiers
Envoi d'un seul fichier
Future<UploadResponse?> uploadAvatar(String filePath) async {
return await upload<UploadResponse>(
'/upload',
filePath: filePath,
fieldName: 'avatar',
additionalFields: {'userId': '123'},
onProgress: (sent, total) {
double progress = sent / total * 100;
print('Progress: ${progress.toStringAsFixed(0)}%');
},
);
}
Envoi de plusieurs fichiers
Future<UploadResponse?> uploadDocuments() async {
return await uploadMultiple<UploadResponse>(
'/upload',
files: {
'avatar': '/path/to/avatar.jpg',
'document': '/path/to/doc.pdf',
},
additionalFields: {'userId': '123'},
onProgress: (sent, total) {
print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
},
);
}
Telechargement de fichiers
Future<void> downloadFile(String url, String savePath) async {
await download(
url,
savePath: savePath,
onProgress: (received, total) {
if (total != -1) {
print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
}
},
deleteOnError: true,
);
}
Intercepteurs
Les intercepteurs vous permettent de modifier les requetes avant leur envoi, de gerer les reponses et de gerer les erreurs. Ils s'executent sur chaque requete effectuee via le service API.
Utilisez les intercepteurs lorsque vous devez :
- Ajouter des en-tetes d'authentification a toutes les requetes
- Journaliser les requetes et reponses pour le debogage
- Transformer les donnees de requete/reponse globalement
- Gerer des codes d'erreur specifiques (ex. rafraichir les jetons sur 401)
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
@override
Map<Type, Interceptor> get interceptors => {
...super.interceptors,
BearerAuthInterceptor: BearerAuthInterceptor(),
LoggingInterceptor: LoggingInterceptor(),
};
...
}
Creer un intercepteur personnalise
metro make:interceptor logging
Fichier : app/networking/dio/interceptors/logging_interceptor.dart
import 'package:nylo_framework/nylo_framework.dart';
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
handler.next(response);
}
@override
void onError(DioException dioException, ErrorInterceptorHandler handler) {
print('ERROR[${dioException.response?.statusCode}] => PATH: ${dioException.requestOptions.path}');
handler.next(dioException);
}
}
Journal reseau
Nylo Website inclut un intercepteur NetworkLogger integre. Il est active par defaut lorsque APP_DEBUG est true dans votre environnement.
Configuration
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
useNetworkLogger: true,
networkLogger: NetworkLogger(
logLevel: LogLevelType.verbose,
request: true,
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
),
);
}
Vous pouvez le desactiver en definissant useNetworkLogger: false.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext})
: super(
buildContext,
decoders: modelDecoders,
useNetworkLogger: false, // <-- Desactiver le journal
);
Niveaux de journalisation
| Niveau | Description |
|---|---|
LogLevelType.verbose |
Afficher tous les details de la requete/reponse |
LogLevelType.minimal |
Afficher uniquement la methode, l'URL, le statut et le temps |
LogLevelType.none |
Aucune sortie de journalisation |
Filtrage des journaux
NetworkLogger(
filter: (options, args) {
// Only log requests to specific endpoints
return options.path.contains('/api/v1');
},
)
Utiliser un service API
Il existe deux facons d'appeler votre service API depuis une page.
Instanciation directe
class _MyHomePageState extends NyPage<MyHomePage> {
ApiService _apiService = ApiService();
@override
get init => () async {
List<User>? users = await _apiService.fetchUsers();
print(users);
};
}
Utiliser le helper api()
Le helper api cree des instances en utilisant vos apiDecoders depuis config/decoders.dart :
class _MyHomePageState extends NyPage<MyHomePage> {
@override
get init => () async {
User? user = await api<ApiService>((request) => request.fetchUser());
print(user);
};
}
Avec des callbacks :
await api<ApiService>(
(request) => request.fetchUser(),
onSuccess: (response, data) {
// data is the morphed User? instance
},
onError: (DioException dioException) {
// Handle the error
},
);
Parametres du helper api()
| Parametre | Type | Description |
|---|---|---|
request |
Function(T) |
La fonction de requete API |
context |
BuildContext? |
Contexte de construction |
headers |
Map<String, dynamic> |
En-tetes supplementaires |
bearerToken |
String? |
Jeton Bearer |
baseUrl |
String? |
Remplacer l'URL de base |
page |
int? |
Page de pagination |
perPage |
int? |
Elements par page |
retry |
int |
Tentatives de relance |
retryDelay |
Duration? |
Delai entre les relances |
onSuccess |
Function(Response, dynamic)? |
Callback de succes |
onError |
Function(DioException)? |
Callback d'erreur |
cacheKey |
String? |
Cle de cache |
cacheDuration |
Duration? |
Duree du cache |
Creer un service API
Pour creer un nouveau service API :
metro make:api_service user
Avec un modele :
metro make:api_service user --model="User"
Cela cree un service API avec des methodes CRUD :
class UserApiService extends NyApiService {
...
Future<List<User>?> fetchAll({dynamic query}) async {
return await network<List<User>>(
request: (request) => request.get("/endpoint-path", queryParameters: query),
);
}
Future<User?> find({required int id}) async {
return await network<User>(
request: (request) => request.get("/endpoint-path/$id"),
);
}
Future<User?> create({required dynamic data}) async {
return await network<User>(
request: (request) => request.post("/endpoint-path", data: data),
);
}
Future<User?> update({dynamic query}) async {
return await network<User>(
request: (request) => request.put("/endpoint-path", queryParameters: query),
);
}
Future<bool?> delete({required int id}) async {
return await network<bool>(
request: (request) => request.delete("/endpoint-path/$id"),
);
}
}
Transformation du JSON en modeles
Le "morphing" est le terme de Nylo Website pour la conversion automatique des reponses JSON en classes de modeles Dart. Lorsque vous utilisez network<User>(...), le JSON de la reponse est passe a travers votre decodeur pour creer une instance User — aucun parsing manuel necessaire.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
// Returns a single User
Future<User?> fetchUser() async {
return await network<User>(
request: (request) => request.get("/user/1"),
);
}
// Returns a List of Users
Future<List<User>?> fetchUsers() async {
return await network<List<User>>(
request: (request) => request.get("/users"),
);
}
}
Les decodeurs sont definis dans lib/bootstrap/decoders.dart :
final Map<Type, dynamic> modelDecoders = {
User: (data) => User.fromJson(data),
List<User>: (data) =>
List.from(data).map((json) => User.fromJson(json)).toList(),
};
Le parametre de type que vous passez a network<T>() est compare a votre map modelDecoders pour trouver le bon decodeur.
Voir aussi : Decoders pour les details sur l'enregistrement des decodeurs de modeles.
Mise en cache des reponses
Mettez en cache les reponses pour reduire les appels API et ameliorer les performances. La mise en cache est utile pour les donnees qui changent rarement, comme les listes de pays, les categories ou la configuration.
Fournissez une cacheKey et une cacheDuration optionnelle :
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
) ?? [];
}
Vider le cache
// Clear a specific cache key
await apiService.clearCache("app_countries");
// Clear all API cache
await apiService.clearAllCache();
Mise en cache avec le helper api()
api<ApiService>(
(request) => request.fetchCountries(),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
);
Politiques de cache
Utilisez CachePolicy pour un controle fin du comportement de mise en cache :
| Politique | Description |
|---|---|
CachePolicy.networkOnly |
Toujours recuperer depuis le reseau (par defaut) |
CachePolicy.cacheFirst |
Essayer le cache d'abord, se rabattre sur le reseau |
CachePolicy.networkFirst |
Essayer le reseau d'abord, se rabattre sur le cache |
CachePolicy.cacheOnly |
Utiliser uniquement le cache, erreur si vide |
CachePolicy.staleWhileRevalidate |
Retourner le cache immediatement, mettre a jour en arriere-plan |
Utilisation
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
cachePolicy: CachePolicy.staleWhileRevalidate,
) ?? [];
}
Quand utiliser chaque politique
- cacheFirst — Donnees qui changent rarement. Retourne les donnees en cache instantanement, ne recupere depuis le reseau que si le cache est vide.
- networkFirst — Donnees qui doivent etre fraiches quand c'est possible. Essaie le reseau d'abord, se rabat sur le cache en cas d'echec.
- staleWhileRevalidate — Interface qui necessite une reponse immediate mais doit rester a jour. Retourne les donnees en cache, puis les rafraichit en arriere-plan.
- cacheOnly — Mode hors ligne. Lance une erreur si aucune donnee en cache n'existe.
Remarque : Si vous fournissez une
cacheKeyoucacheDurationsans specifier decachePolicy, la politique par defaut estcacheFirst.
Relancer les requetes echouees
Relancez automatiquement les requetes qui echouent.
Relance basique
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
);
}
Relance avec delai
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
retryDelay: Duration(seconds: 2),
);
}
Relance conditionnelle
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
retryIf: (DioException dioException) {
// Only retry on server errors
return dioException.response?.statusCode == 500;
},
);
}
Relance au niveau du service
apiService.setRetry(3);
apiService.setRetryDelay(Duration(seconds: 2));
apiService.setRetryIf((dioException) => dioException.response?.statusCode == 500);
Verification de la connectivite
Echouez rapidement lorsque l'appareil est hors ligne au lieu d'attendre un delai d'expiration.
Au niveau du service
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
@override
bool get checkConnectivityBeforeRequest => true;
...
}
Par requete
await network(
request: (request) => request.get("/users"),
checkConnectivity: true,
);
Dynamique
apiService.setCheckConnectivityBeforeRequest(true);
Lorsque c'est active et que l'appareil est hors ligne :
- La politique
networkFirstse rabat sur le cache - Les autres politiques lancent immediatement
DioExceptionType.connectionError
Jetons d'annulation
Gerez et annulez les requetes en attente.
// Create a managed cancel token
final token = apiService.createCancelToken();
await apiService.get('/endpoint', cancelToken: token);
// Cancel all pending requests (e.g., on logout)
apiService.cancelAllRequests('User logged out');
// Check active request count
int count = apiService.activeRequestCount;
// Clean up a specific token when done
apiService.removeCancelToken(token);
Definir les en-tetes d'authentification
Remplacez setAuthHeaders pour attacher des en-tetes d'authentification a chaque requete. Cette methode est appelee avant chaque requete lorsque shouldSetAuthHeaders est true (la valeur par defaut).
class ApiService extends NyApiService {
...
@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
String? myAuthToken = Auth.data(field: 'token');
if (myAuthToken != null) {
headers.addBearerToken(myAuthToken);
}
return headers;
}
}
Desactiver les en-tetes d'authentification
Pour les points d'acces publics qui n'ont pas besoin d'authentification :
// Per-request
await network(
request: (request) => request.get("/public-endpoint"),
shouldSetAuthHeaders: false,
);
// Service-level
apiService.setShouldSetAuthHeaders(false);
Voir aussi : Authentification pour les details sur l'authentification des utilisateurs et le stockage des jetons.
Rafraichir les jetons
Remplacez shouldRefreshToken et refreshToken pour gerer l'expiration des jetons. Ils sont appeles avant chaque requete.
class ApiService extends NyApiService {
...
@override
Future<bool> shouldRefreshToken() async {
// Check if the token needs refreshing
return false;
}
@override
Future<void> refreshToken(Dio dio) async {
// Use the fresh Dio instance (no interceptors) to refresh the token
dynamic response = (await dio.post("https://example.com/refresh-token")).data;
// Save the new token to storage
await Auth.set((data) {
data['token'] = response['token'];
return data;
});
}
}
Le parametre dio dans refreshToken est une nouvelle instance Dio, separee de l'instance principale du service, pour eviter les boucles d'intercepteurs.
Service API singleton
Par defaut, le helper api cree une nouvelle instance a chaque fois. Pour utiliser un singleton, passez une instance au lieu d'une factory dans config/decoders.dart :
final Map<Type, dynamic> apiDecoders = {
ApiService: () => ApiService(), // New instance each time
ApiService: ApiService(), // Singleton — same instance always
};
Configuration avancee
Initialisation Dio personnalisee
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
initDio: (Dio dio) {
dio.options.validateStatus = (status) => status! < 500;
return dio;
},
);
}
Acceder a l'instance Dio
Dio dioInstance = apiService.dio;
Response response = await dioInstance.request(
'/custom-endpoint',
options: Options(method: 'OPTIONS'),
);
Aide a la pagination
apiService.setPagination(
1,
paramPage: 'page',
paramPerPage: 'per_page',
perPage: '20',
);
Callbacks d'evenements
apiService.onSuccess((response, data) {
print('Success: ${response.statusCode}');
});
apiService.onError((dioException) {
print('Error: ${dioException.message}');
});
Proprietes remplacables
| Propriete | Type | Par defaut | Description |
|---|---|---|---|
baseUrl |
String |
"" |
URL de base pour toutes les requetes |
interceptors |
Map<Type, Interceptor> |
{} |
Intercepteurs Dio |
decoders |
Map<Type, dynamic>? |
{} |
Decodeurs de modeles pour la transformation JSON |
shouldSetAuthHeaders |
bool |
true |
Si setAuthHeaders doit etre appele avant les requetes |
retry |
int |
0 |
Tentatives de relance par defaut |
retryDelay |
Duration |
1 second |
Delai par defaut entre les relances |
checkConnectivityBeforeRequest |
bool |
false |
Verifier la connectivite avant les requetes |