Effettuare chiamate HTTP in Flutter con Dio: intercettori e gestione errori
GuideIntermedio35 min Flutter 3.x

Effettuare chiamate HTTP in Flutter con Dio: intercettori e gestione errori

Perché Dio?

Il pacchetto http va benissimo per richieste semplici, ma quando il progetto cresce servono funzionalità più avanzate: intercettori per aggiungere automaticamente token di autenticazione, gestione centralizzata degli errori, timeout configurabili e logging delle richieste.

Dio è uno dei client HTTP più potenti dell'ecosistema Flutter e risolve tutti questi problemi con un'API pulita.

In questo tutorial costruiremo un piccolo servizio che recupera dei post da una API pubblica, aggiungendo intercettori per il logging e l'autenticazione, oltre a una strategia robusta per la gestione delle eccezioni.

  1. 1

    Aggiungere la dipendenza Dio

    Aggiungi il pacchetto dio al tuo progetto. Puoi farlo da terminale con il comando flutter pub add dio oppure inserendo manualmente la dipendenza nel file pubspec.yaml.

    Dopo l'aggiunta, esegui flutter pub get per scaricare il pacchetto.

    dependencies:
      flutter:
        sdk: flutter
      dio: ^5.4.0

    Risultato atteso

    Il pacchetto Dio è installato e pronto per essere importato nel progetto.

  2. 2

    Creare un'istanza Dio configurata

    Invece di creare nuove istanze sparse nel codice, è buona pratica centralizzare la configurazione di Dio in una classe dedicata. Qui impostiamo la baseUrl, i timeout di connessione e ricezione, e gli header di default.

    Usiamo come esempio l'API pubblica JSONPlaceholder.

    import 'package:dio/dio.dart';
    
    class ApiClient {
      final Dio dio;
    
      ApiClient()
          : dio = Dio(
              BaseOptions(
                baseUrl: 'https://jsonplaceholder.typicode.com',
                connectTimeout: const Duration(seconds: 10),
                receiveTimeout: const Duration(seconds: 10),
                headers: {
                  'Content-Type': 'application/json',
                  'Accept': 'application/json',
                },
              ),
            );
    }

    Risultato atteso

    Hai una classe ApiClient con un'istanza Dio configurata con base URL e timeout.

  3. 3

    Aggiungere un intercettore di logging

    Gli intercettori permettono di eseguire codice prima dell'invio di una richiesta, alla ricezione di una risposta o in caso di errore. Un primo uso utile è il logging, per vedere in console le richieste effettuate.

    Usiamo InterceptorsWrapper e lo aggiungiamo alla lista dio.interceptors.

    ApiClient() : dio = Dio(/* ... BaseOptions ... */) {
      dio.interceptors.add(
        InterceptorsWrapper(
          onRequest: (options, handler) {
            print('--> ${options.method} ${options.uri}');
            return handler.next(options);
          },
          onResponse: (response, handler) {
            print('<-- ${response.statusCode} ${response.requestOptions.uri}');
            return handler.next(response);
          },
          onError: (error, handler) {
            print('ERROR <-- ${error.response?.statusCode} ${error.requestOptions.uri}');
            return handler.next(error);
          },
        ),
      );
    }

    Risultato atteso

    Ogni richiesta, risposta ed errore viene stampato nella console di debug.

  4. 4

    Aggiungere un intercettore per il token di autenticazione

    Un secondo intercettore tipico inietta automaticamente il token di autenticazione in ogni richiesta, senza doverlo ripetere ad ogni chiamata. In un'app reale il token verrebbe letto da uno storage sicuro (es. flutter_secure_storage).

    Qui simuliamo il recupero del token con un metodo _getToken().

    Future<String?> _getToken() async {
      // In un'app reale: leggi da secure storage
      return 'fake-jwt-token';
    }
    
    void _setupAuthInterceptor() {
      dio.interceptors.add(
        InterceptorsWrapper(
          onRequest: (options, handler) async {
            final token = await _getToken();
            if (token != null) {
              options.headers['Authorization'] = 'Bearer $token';
            }
            return handler.next(options);
          },
        ),
      );
    }

    Risultato atteso

    Tutte le richieste includono automaticamente l'header Authorization quando il token è disponibile.

  5. 5

    Gestire gli errori in modo centralizzato

    Dio lancia eccezioni di tipo DioException. Creiamo una classe di eccezione personalizzata e una funzione che traduce i vari tipi di errore (timeout, errori di rete, codici di stato HTTP) in messaggi comprensibili per l'utente.

    Questo evita di duplicare la logica di gestione errori in ogni chiamata.

    class ApiException implements Exception {
      final String message;
      final int? statusCode;
      ApiException(this.message, {this.statusCode});
    
      @override
      String toString() => message;
    }
    
    ApiException handleDioError(DioException e) {
      switch (e.type) {
        case DioExceptionType.connectionTimeout:
        case DioExceptionType.receiveTimeout:
        case DioExceptionType.sendTimeout:
          return ApiException('Tempo di attesa scaduto. Riprova.');
        case DioExceptionType.connectionError:
          return ApiException('Nessuna connessione a Internet.');
        case DioExceptionType.badResponse:
          final code = e.response?.statusCode;
          if (code == 404) return ApiException('Risorsa non trovata.', statusCode: 404);
          if (code == 401) return ApiException('Non autorizzato.', statusCode: 401);
          return ApiException('Errore del server ($code).', statusCode: code);
        default:
          return ApiException('Si è verificato un errore imprevisto.');
      }
    }

    Risultato atteso

    Hai una funzione che converte qualsiasi DioException in un'ApiException con messaggio leggibile.

  6. 6

    Effettuare una chiamata GET con modello tipizzato

    Mettiamo tutto insieme: definiamo un modello Post, e un metodo fetchPosts() che effettua la richiesta GET, mappa la risposta JSON in oggetti Dart e gestisce gli errori usando la funzione del passo precedente.

    Nota l'uso di try/catch per intercettare DioException.

    class Post {
      final int id;
      final String title;
      final String body;
      Post({required this.id, required this.title, required this.body});
    
      factory Post.fromJson(Map<String, dynamic> json) => Post(
            id: json['id'],
            title: json['title'],
            body: json['body'],
          );
    }
    
    extension PostApi on ApiClient {
      Future<List<Post>> fetchPosts() async {
        try {
          final response = await dio.get('/posts');
          final data = response.data as List;
          return data.map((e) => Post.fromJson(e)).toList();
        } on DioException catch (e) {
          throw handleDioError(e);
        }
      }
    }

    Risultato atteso

    Il metodo restituisce una lista di Post oppure lancia un'ApiException con messaggio chiaro.

  7. 7

    Usare il servizio nella UI

    Infine, usiamo FutureBuilder per mostrare i dati nella UI, gestendo i tre stati: caricamento, errore e successo. Grazie alla gestione centralizzata, il messaggio d'errore mostrato all'utente è già leggibile.

    class PostsScreen extends StatefulWidget {
      const PostsScreen({super.key});
      @override
      State<PostsScreen> createState() => _PostsScreenState();
    }
    
    class _PostsScreenState extends State<PostsScreen> {
      final _api = ApiClient();
      late final Future<List<Post>> _future = _api.fetchPosts();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Post')),
          body: FutureBuilder<List<Post>>(
            future: _future,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              }
              if (snapshot.hasError) {
                return Center(child: Text(snapshot.error.toString()));
              }
              final posts = snapshot.data!;
              return ListView.builder(
                itemCount: posts.length,
                itemBuilder: (context, i) => ListTile(
                  title: Text(posts[i].title),
                  subtitle: Text(posts[i].body, maxLines: 2, overflow: TextOverflow.ellipsis),
                ),
              );
            },
          ),
        );
      }
    }

    Risultato atteso

    L'app mostra la lista dei post, un indicatore di caricamento durante la richiesta e un messaggio chiaro in caso di errore.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!