Basics

Networking

Introduzione

Nylo Website rende il networking semplice. Definisci gli endpoint API in classi di servizio che estendono NyApiService, poi li chiami dalle tue pagine. Il framework gestisce la decodifica JSON, la gestione degli errori, il caching e la conversione automatica delle risposte nelle tue classi modello (chiamata "morphing").

I tuoi API service risiedono in lib/app/networking/. Un progetto nuovo include un ApiService predefinito:

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"),
    );
  }
}

Ci sono tre modi per effettuare richieste HTTP:

Approccio Restituisce Ideale Per
Metodi di convenienza (get, post, ecc.) T? Operazioni CRUD semplici
network() T? Richieste che necessitano di caching, ripetizione o header personalizzati
networkResponse() NyResponse<T> Quando servono codici di stato, header o dettagli degli errori

Sotto il cofano, Nylo Website utilizza Dio, un potente client HTTP.

Metodi di Convenienza

NyApiService fornisce metodi abbreviati per le operazioni HTTP comuni. Questi chiamano internamente network().

Richiesta GET

Future<User?> fetchUser(int id) async {
  return await get<User>(
    "/users/$id",
    queryParameters: {"include": "profile"},
  );
}

Richiesta POST

Future<User?> createUser(Map<String, dynamic> data) async {
  return await post<User>("/users", data: data);
}

Richiesta PUT

Future<User?> updateUser(int id, Map<String, dynamic> data) async {
  return await put<User>("/users/$id", data: data);
}

Richiesta DELETE

Future<bool?> deleteUser(int id) async {
  return await delete<bool>("/users/$id");
}

Richiesta PATCH

Future<User?> patchUser(int id, Map<String, dynamic> data) async {
  return await patch<User>("/users/$id", data: data);
}

Richiesta HEAD

Usa HEAD per verificare l'esistenza di una risorsa o ottenere gli header senza scaricare il corpo:

Future<bool> checkResourceExists(String url) async {
  Response response = await head(url);
  return response.statusCode == 200;
}

Helper Network

Il metodo network offre maggiore controllo rispetto ai metodi di convenienza. Restituisce direttamente i dati convertiti (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),
    );
  }
}

La callback request riceve un'istanza Dio con il tuo URL base e gli interceptor già configurati.

Parametri di network

Parametro Tipo Descrizione
request Function(Dio) La richiesta HTTP da eseguire (obbligatorio)
bearerToken String? Bearer token per questa richiesta
baseUrl String? Sovrascrive l'URL base del servizio
headers Map<String, dynamic>? Header aggiuntivi
retry int? Numero di tentativi di ripetizione
retryDelay Duration? Ritardo tra i tentativi
retryIf bool Function(DioException)? Condizione per la ripetizione
connectionTimeout Duration? Timeout di connessione
receiveTimeout Duration? Timeout di ricezione
sendTimeout Duration? Timeout di invio
cacheKey String? Chiave di cache
cacheDuration Duration? Durata della cache
cachePolicy CachePolicy? Strategia di cache
checkConnectivity bool? Controlla la connettività prima della richiesta
handleSuccess Function(NyResponse<T>)? Callback di successo
handleFailure Function(NyResponse<T>)? Callback di fallimento

Helper networkResponse

Usa networkResponse quando hai bisogno di accedere alla risposta completa - codici di stato, header, messaggi di errore - non solo ai dati. Restituisce un NyResponse<T> invece di T?.

Usa networkResponse quando hai bisogno di:

  • Controllare i codici di stato HTTP per una gestione specifica
  • Accedere agli header della risposta
  • Ottenere messaggi di errore dettagliati per il feedback dell'utente
  • Implementare una logica di gestione errori personalizzata
Future<NyResponse<User>> fetchUser(int id) async {
  return await networkResponse<User>(
    request: (request) => request.get("/users/$id"),
  );
}

Poi usa la risposta nella tua pagina:

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() — restituisce i dati direttamente
User? user = await network<User>(
  request: (request) => request.get("/users/1"),
);

// networkResponse() — restituisce la risposta completa
NyResponse<User> response = await networkResponse<User>(
  request: (request) => request.get("/users/1"),
);
User? user = response.data;
int? status = response.statusCode;

Entrambi i metodi accettano gli stessi parametri. Scegli networkResponse quando hai bisogno di ispezionare la risposta oltre ai semplici dati.

NyResponse

NyResponse<T> avvolge la risposta Dio con dati convertiti e helper per lo stato.

Proprietà

Proprietà Tipo Descrizione
response Response? Risposta Dio originale
data T? Dati convertiti/decodificati
rawData dynamic Dati grezzi della risposta
headers Headers? Header della risposta
statusCode int? Codice di stato HTTP
statusMessage String? Messaggio di stato HTTP
contentType String? Tipo di contenuto dagli header
errorMessage String? Messaggio di errore estratto

Controlli di Stato

Getter Descrizione
isSuccessful Stato 200-299
isClientError Stato 400-499
isServerError Stato 500-599
isRedirect Stato 300-399
hasData I dati non sono null
isUnauthorized Stato 401
isForbidden Stato 403
isNotFound Stato 404
isTimeout Stato 408
isConflict Stato 409
isRateLimited Stato 429

Helper per i Dati

NyResponse<User> response = await apiService.fetchUser(1);

// Ottieni i dati o lancia un'eccezione se null
User user = response.dataOrThrow('User not found');

// Ottieni i dati o usa un fallback
User user = response.dataOr(User.guest());

// Esegui callback solo se riuscito
String? greeting = response.ifSuccessful((user) => 'Hello ${user.name}');

// Pattern match su successo/fallimento
String result = response.when(
  success: (user) => 'Welcome, ${user.name}!',
  failure: (response) => 'Error: ${response.statusMessage}',
);

// Ottieni un header specifico
String? authHeader = response.getHeader('Authorization');

Opzioni Base

Configura le opzioni Dio predefinite per il tuo API service utilizzando il parametro 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);
    },
  );
  ...
}

Puoi anche configurare le opzioni dinamicamente su un'istanza:

apiService.setConnectTimeout(Duration(seconds: 10));
apiService.setReceiveTimeout(Duration(seconds: 30));
apiService.setSendTimeout(Duration(seconds: 10));
apiService.setContentType('application/json');

Clicca qui per visualizzare tutte le opzioni base che puoi impostare.

Aggiungere Header

Header Per Richiesta

Future fetchWithHeaders() async => await network(
  request: (request) => request.get("/test"),
  headers: {
    "Authorization": "Bearer aToken123",
    "Device": "iPhone"
  }
);

Bearer Token

Future fetchUser() async => await network(
  request: (request) => request.get("/user"),
  bearerToken: "hello-world-123",
);

Header a Livello di Servizio

apiService.setHeaders({"X-Custom-Header": "value"});
apiService.setBearerToken("my-token");

Estensione RequestHeaders

Il tipo RequestHeaders (un typedef Map<String, dynamic>) fornisce metodi helper:

@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;
}
Metodo Descrizione
addBearerToken(token) Imposta l'header Authorization: Bearer
getBearerToken() Legge il bearer token dagli header
addHeader(key, value) Aggiunge un header personalizzato
hasHeader(key) Verifica se un header esiste

Caricamento File

Caricamento File Singolo

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)}%');
    },
  );
}

Caricamento File Multipli

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)}%');
    },
  );
}

Download File

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,
  );
}

Interceptor

Gli interceptor ti permettono di modificare le richieste prima dell'invio, gestire le risposte e gestire gli errori. Vengono eseguiti su ogni richiesta effettuata tramite l'API service.

Usa gli interceptor quando hai bisogno di:

  • Aggiungere header di autenticazione a tutte le richieste
  • Registrare richieste e risposte per il debug
  • Trasformare i dati di richiesta/risposta globalmente
  • Gestire codici di errore specifici (es. aggiornare i token su 401)
class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  @override
  Map<Type, Interceptor> get interceptors => {
    ...super.interceptors,
    BearerAuthInterceptor: BearerAuthInterceptor(),
    LoggingInterceptor: LoggingInterceptor(),
  };
  ...
}

Creare un Interceptor Personalizzato

metro make:interceptor logging

File: 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);
  }
}

Logger di Rete

Nylo Website include un interceptor NetworkLogger integrato. È abilitato per impostazione predefinita quando APP_DEBUG è true nel tuo ambiente.

Configurazione

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,
    ),
  );
}

Puoi disabilitarlo impostando useNetworkLogger: false.

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext})
      : super(
          buildContext,
          decoders: modelDecoders,
          useNetworkLogger: false, // <-- Disabilita il logger
        );

Livelli di Log

Livello Descrizione
LogLevelType.verbose Stampa tutti i dettagli di richiesta/risposta
LogLevelType.minimal Stampa solo metodo, URL, stato e tempo
LogLevelType.none Nessun output di log

Filtrare i Log

NetworkLogger(
  filter: (options, args) {
    // Only log requests to specific endpoints
    return options.path.contains('/api/v1');
  },
)

Utilizzare un API Service

Ci sono due modi per chiamare il tuo API service da una pagina.

Istanziazione Diretta

class _MyHomePageState extends NyPage<MyHomePage> {

  ApiService _apiService = ApiService();

  @override
  get init => () async {
    List<User>? users = await _apiService.fetchUsers();
    print(users);
  };
}

Utilizzare l'Helper api()

L'helper api crea istanze utilizzando i tuoi apiDecoders da config/decoders.dart:

class _MyHomePageState extends NyPage<MyHomePage> {

  @override
  get init => () async {
    User? user = await api<ApiService>((request) => request.fetchUser());
    print(user);
  };
}

Con callback:

await api<ApiService>(
  (request) => request.fetchUser(),
  onSuccess: (response, data) {
    // data is the morphed User? instance
  },
  onError: (DioException dioException) {
    // Handle the error
  },
);

Parametri dell'Helper api()

Parametro Tipo Descrizione
request Function(T) La funzione di richiesta API
context BuildContext? Build context
headers Map<String, dynamic> Header aggiuntivi
bearerToken String? Bearer token
baseUrl String? Sovrascrive l'URL base
page int? Pagina di paginazione
perPage int? Elementi per pagina
retry int Tentativi di ripetizione
retryDelay Duration? Ritardo tra i tentativi
onSuccess Function(Response, dynamic)? Callback di successo
onError Function(DioException)? Callback di errore
cacheKey String? Chiave di cache
cacheDuration Duration? Durata della cache

Creare un API Service

Per creare un nuovo API service:

metro make:api_service user

Con un modello:

metro make:api_service user --model="User"

Questo crea un API service con metodi 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"),
    );
  }
}

Conversione JSON in Modelli

"Morphing" è il termine di Nylo Website per la conversione automatica delle risposte JSON nelle tue classi modello Dart. Quando usi network<User>(...), il JSON della risposta viene passato attraverso il tuo decoder per creare un'istanza User -- nessun parsing manuale necessario.

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  // Restituisce un singolo User
  Future<User?> fetchUser() async {
    return await network<User>(
      request: (request) => request.get("/user/1"),
    );
  }

  // Restituisce una Lista di User
  Future<List<User>?> fetchUsers() async {
    return await network<List<User>>(
      request: (request) => request.get("/users"),
    );
  }
}

I decoder sono definiti in 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(),
};

Il parametro di tipo che passi a network<T>() viene confrontato con la tua mappa modelDecoders per trovare il decoder corretto.

Vedi anche: Decoders per dettagli sulla registrazione dei model decoder.

Cache delle Risposte

Metti in cache le risposte per ridurre le chiamate API e migliorare le prestazioni. Il caching è utile per dati che non cambiano frequentemente, come elenchi di paesi, categorie o configurazioni.

Fornisci una cacheKey e opzionalmente una cacheDuration:

Future<List<Country>> fetchCountries() async {
  return await network<List<Country>>(
    request: (request) => request.get("/countries"),
    cacheKey: "app_countries",
    cacheDuration: const Duration(hours: 1),
  ) ?? [];
}

Svuotare la Cache

// Clear a specific cache key
await apiService.clearCache("app_countries");

// Clear all API cache
await apiService.clearAllCache();

Caching con l'Helper api()

api<ApiService>(
  (request) => request.fetchCountries(),
  cacheKey: "app_countries",
  cacheDuration: const Duration(hours: 1),
);

Politiche di Cache

Usa CachePolicy per un controllo granulare sul comportamento della cache:

Politica Descrizione
CachePolicy.networkOnly Recupera sempre dalla rete (predefinito)
CachePolicy.cacheFirst Prova prima la cache, poi la rete come fallback
CachePolicy.networkFirst Prova prima la rete, poi la cache come fallback
CachePolicy.cacheOnly Usa solo la cache, errore se vuota
CachePolicy.staleWhileRevalidate Restituisce la cache immediatamente, aggiorna in background

Utilizzo

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,
  ) ?? [];
}

Quando Usare Ogni Politica

  • cacheFirst -- Dati che cambiano raramente. Restituisce i dati in cache istantaneamente, recupera dalla rete solo se la cache è vuota.
  • networkFirst -- Dati che dovrebbero essere freschi quando possibile. Prova prima la rete, poi la cache come fallback in caso di errore.
  • staleWhileRevalidate -- UI che necessita di una risposta immediata ma che deve rimanere aggiornata. Restituisce i dati in cache, poi aggiorna in background.
  • cacheOnly -- Modalità offline. Lancia un errore se non esistono dati in cache.

Nota: Se fornisci una cacheKey o cacheDuration senza specificare una cachePolicy, la politica predefinita è cacheFirst.

Ripetizione delle Richieste Fallite

Ripeti automaticamente le richieste che falliscono.

Ripetizione Base

Future fetchUsers() async {
  return await network(
    request: (request) => request.get("/users"),
    retry: 3,
  );
}

Ripetizione con Ritardo

Future fetchUsers() async {
  return await network(
    request: (request) => request.get("/users"),
    retry: 3,
    retryDelay: Duration(seconds: 2),
  );
}

Ripetizione Condizionale

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;
    },
  );
}

Ripetizione a Livello di Servizio

apiService.setRetry(3);
apiService.setRetryDelay(Duration(seconds: 2));
apiService.setRetryIf((dioException) => dioException.response?.statusCode == 500);

Controlli di Connettività

Fallisci rapidamente quando il dispositivo è offline invece di attendere un timeout.

A Livello di Servizio

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  @override
  bool get checkConnectivityBeforeRequest => true;
  ...
}

Per Richiesta

await network(
  request: (request) => request.get("/users"),
  checkConnectivity: true,
);

Dinamico

apiService.setCheckConnectivityBeforeRequest(true);

Quando abilitato e il dispositivo è offline:

  • La politica networkFirst ricorre alla cache
  • Le altre politiche lanciano DioExceptionType.connectionError immediatamente

Token di Cancellazione

Gestisci e cancella le richieste in sospeso.

// 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);

Impostare Header di Autenticazione

Sovrascrivi setAuthHeaders per allegare header di autenticazione a ogni richiesta. Questo metodo viene chiamato prima di ogni richiesta quando shouldSetAuthHeaders è true (impostazione predefinita).

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;
  }
}

Disabilitare gli Header di Autenticazione

Per endpoint pubblici che non necessitano di autenticazione:

// Per richiesta
await network(
  request: (request) => request.get("/public-endpoint"),
  shouldSetAuthHeaders: false,
);

// A livello di servizio
apiService.setShouldSetAuthHeaders(false);

Vedi anche: Autenticazione per dettagli sull'autenticazione degli utenti e la memorizzazione dei token.

Aggiornamento dei Token

Sovrascrivi shouldRefreshToken e refreshToken per gestire la scadenza dei token. Questi vengono chiamati prima di ogni richiesta.

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;
    });
  }
}

Il parametro dio in refreshToken è una nuova istanza Dio, separata dall'istanza principale del servizio, per evitare loop di interceptor.

API Service Singleton

Per impostazione predefinita, l'helper api crea una nuova istanza ogni volta. Per usare un singleton, passa un'istanza invece di una factory in config/decoders.dart:

final Map<Type, dynamic> apiDecoders = {
  ApiService: () => ApiService(), // New instance each time

  ApiService: ApiService(), // Singleton — same instance always
};

Configurazione Avanzata

Inizializzazione Dio Personalizzata

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(
    buildContext,
    decoders: modelDecoders,
    initDio: (Dio dio) {
      dio.options.validateStatus = (status) => status! < 500;
      return dio;
    },
  );
}

Accesso all'Istanza Dio

Dio dioInstance = apiService.dio;

Response response = await dioInstance.request(
  '/custom-endpoint',
  options: Options(method: 'OPTIONS'),
);

Helper per la Paginazione

apiService.setPagination(
  1,
  paramPage: 'page',
  paramPerPage: 'per_page',
  perPage: '20',
);

Callback degli Eventi

apiService.onSuccess((response, data) {
  print('Success: ${response.statusCode}');
});

apiService.onError((dioException) {
  print('Error: ${dioException.message}');
});

Proprietà Sovrascrivibili

Proprietà Tipo Predefinito Descrizione
baseUrl String "" URL base per tutte le richieste
interceptors Map<Type, Interceptor> {} Interceptor Dio
decoders Map<Type, dynamic>? {} Decoder dei modelli per la conversione JSON
shouldSetAuthHeaders bool true Se chiamare setAuthHeaders prima delle richieste
retry int 0 Tentativi di ripetizione predefiniti
retryDelay Duration 1 second Ritardo predefinito tra i tentativi
checkConnectivityBeforeRequest bool false Controlla la connettività prima delle richieste