Basics

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 cacheKey ou cacheDuration sans specifier de cachePolicy, la politique par defaut est cacheFirst.

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 networkFirst se 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