Il tuo e-commerce o la tua app gestionale carica i dati una volta, poi quando l'utente tira per aggiornare, si blocca. O peggio: una chiamata API va in timeout e l'app si pianta. Se stai sviluppando con Flutter, il problema non è il framework: è come gestisci le richieste HTTP e lo stato che ne deriva. Noi, di Meteora Web, lo vediamo ogni giorno nei progetti che ci arrivano per audit. E la soluzione, quasi sempre, passa da due scelte precise: un client HTTP robusto come Dio e un pattern di gestione stati che non collassi sotto pressione.
Perché il pacchetto http non basta più quando le API crescono?
Il pacchetto http ufficiale di Dart funziona. Per una demo, è perfetto. Ma quando devi gestire token di autenticazione, retry automatici, logging delle richieste, upload file con progresso e timeout configurabili per endpoint, ti ritrovi a scrivere centinaia di righe di boilerplate che si ripetono in ogni chiamata. Questo è il problema reale: il codice duplicato è il primo passo verso bug difficili da tracciare.
Dio non è un capriccio: è un client HTTP che incapsula tutto quello che serve in produzione. Interceptor per il token, logging automatico in debug, cancellazione delle richieste quando l'utente naviga via, gestione dei timeout per singola chiamata. Il ritorno è concreto: meno codice da mantenere e un debug più veloce quando qualcosa va storto.
Come configurare Dio in un progetto Flutter reale
Prima di tutto, aggiungi la dipendenza. Poi crea un'istanza condivisa, non una nuova per ogni chiamata. Ecco un esempio che usiamo come base nei nostri progetti:
Sponsored Protocol
import 'package:dio/dio.dart';
class ApiClient {
static final Dio dio = Dio(
BaseOptions(
baseUrl: 'https://api.tuoservizio.it',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
),
)..interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Aggiungi token di autenticazione se presente
final token = AuthStorage.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) {
// Log dell'errore in debug
debugPrint('API Error: ${error.message}');
handler.next(error);
},
),
);
}
Con questa configurazione, ogni chiamata fatta con ApiClient.dio eredita timeout, header e gestione del token. Niente più dimenticanze. E se il token scade, puoi aggiungere un interceptor per il refresh automatico senza toccare il codice delle singole chiamate.
Quale pattern di gestione stati scegliere per le chiamate API?
Qui si gioca la partita vera. Hai tre opzioni principali: setState per casi semplici, Provider per una via di mezzo, Riverpod o Bloc per applicazioni che devono scalare. La scelta dipende da una domanda: quante parti della tua UI devono reagire allo stesso dato? Se la risposta è "più di una", setState non basta.
Sponsored Protocol
Noi, di Meteora Web, abbiamo costruito piattaforme multi-tenant e sappiamo che il pattern giusto riduce i bug di stato e rende il codice testabile. La nostra raccomandazione per chi parte da zero ma ha ambizione di crescita: Riverpod. È moderno, type-safe e si integra bene con Dio.
Come gestire una chiamata API con Riverpod in modo pulito
Il concetto chiave è separare la chiamata HTTP dalla UI. Un provider espone lo stato, la UI lo ascolta. Ecco un esempio pratico:
final prodottiProvider = FutureProvider.autoDispose>((ref) async {
final response = await ApiClient.dio.get('/prodotti');
if (response.statusCode == 200) {
return (response.data as List)
.map((json) => Prodotto.fromJson(json))
.toList();
}
throw Exception('Errore caricamento prodotti');
});
Nella UI, usi ref.watch per ascoltare lo stato e gestisci i tre casi: loading, errore, dati. Ecco il pattern che usiamo:
final prodotti = ref.watch(prodottiProvider);
prodotti.when(
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ProdottoCard(item: items[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Errore: $err')),
);
Con autoDispose, quando la UI non ascolta più il provider, la cache viene liberata. Questo evita memory leak e chiamate API inutili. Il risultato: un'app che non si blocca e non consuma dati a caso.
Come gestire errori e retry senza far andare in crash l'app?
Le API cadono. I server vanno in timeout. La rete mobile è instabile. Se la tua app non gestisce questi scenari, l'utente la chiude e non torna. Il punto non è se un errore capiterà, ma quando. E la differenza tra un'app professionale e una amatoriale sta proprio qui.
Sponsored Protocol
Con Dio, hai due livelli di protezione. Il primo è il retry automatico per errori transitori (come il 503 o il timeout di rete). Il secondo è la gestione centralizzata degli errori negli interceptor, così la UI riceve sempre un messaggio chiaro, mai un'eccezione grezza.
Come implementare il retry automatico con Dio
Ecco un interceptor che riprova la chiamata fino a 3 volte con backoff esponenziale:
class RetryInterceptor extends Interceptor {
final Dio dio;
final int maxRetries;
RetryInterceptor({required this.dio, this.maxRetries = 3});
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.requestOptions.extra['retry'] == true) {
return handler.next(err);
}
if (_isRetryable(err) && err.requestOptions.extra['retryCount'] == null) {
for (int i = 0; i < maxRetries; i++) {
await Future.delayed(Duration(seconds: 2 * (i + 1)));
try {
final response = await dio.request(
err.requestOptions.path,
options: Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers,
extra: {'retry': true, 'retryCount': i + 1},
),
);
return handler.resolve(response);
} catch (_) {}
}
}
handler.next(err);
}
bool _isRetryable(DioException err) {
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError ||
(err.response?.statusCode ?? 0) >= 500;
}
}
Questo codice, una volta aggiunto all'istanza di Dio, protegge tutte le chiamate dell'app. L'utente non vede errori per un calo di rete momentaneo: l'app riprova da sola. E se l'errore persiste, la UI mostra un messaggio chiaro con un bottone per riprovare manualmente.
Sponsored Protocol
Come ottimizzare le chiamate API per ridurre il consumo di dati?
Su mobile, ogni megabyte conta. Soprattutto in Italia, dove i piani dati non sono illimitati per tutti. Se la tua app fa chiamate API aggressive, l'utente te lo fa notare con una recensione negativa. La soluzione è duplice: caching e paginazione.
Come implementare il caching con Dio e Riverpod
Per il caching, puoi usare l'header Cache-Control se il tuo backend lo supporta, oppure un approccio locale. Ecco un esempio di provider che usa la cache in memoria:
final cacheProvider = Provider
Per la paginazione, usa i parametri page e limit nelle query. Non caricare mai liste infinite in una sola chiamata. Un esempio con Dio:
Sponsored Protocol
final response = await ApiClient.dio.get(
'/prodotti',
queryParameters: {'page': page, 'limit': 20},
);
Questo approccio riduce il carico sul server e il consumo di dati del client. E se il backend non supporta la paginazione, parlane con chi lo sviluppa: è una best practice universale.
Cosa fare adesso
Hai le basi per rendere le tue chiamate API robuste. Ora metti in pratica questi passaggi:
1. Sostituisci il pacchetto http con Dio nel tuo progetto. Configura un'istanza condivisa con timeout e interceptor per il token.
2. Adotta Riverpod per la gestione stati. Migra gradualmente: inizia da una feature semplice, poi estendi.
3. Aggiungi il retry automatico per gli errori transitori. Testa con il tuo server in modalità aereo per vedere il comportamento.
4. Implementa caching e paginazione per le liste. Misura il consumo dati prima e dopo con gli strumenti di sviluppo.
Se vuoi approfondire l'architettura completa di un'app Flutter, parti dalla nostra guida pillar su Flutter. E se hai un progetto in produzione che soffre di problemi di rete, sai dove trovarci: lavoriamo su questi problemi ogni giorno.