Perché Retrofit in Flutter

Gestire manualmente le chiamate HTTP con Dio funziona bene, ma man mano che l'app cresce ci si ritrova a scrivere codice ripetitivo per costruire URL, serializzare parametri e deserializzare le risposte. Il pacchetto retrofit risolve questo problema generando automaticamente un client REST tipizzato a partire da un'interfaccia annotata, appoggiandosi a Dio per il trasporto e a json_serializable per la serializzazione.

Il risultato è un layer di rete dichiarativo, leggibile e a prova di refactoring: definisci cosa vuoi chiamare, non come.

Installazione

Aggiungi le dipendenze al pubspec.yaml:

dependencies:
  dio: ^5.4.0
  retrofit: ^4.1.0
  json_annotation: ^4.9.0

dev_dependencies:
  build_runner: ^2.4.8
  retrofit_generator: ^8.1.0
  json_serializable: ^6.7.1

Definire i modelli

Partiamo da un semplice modello serializzabile:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Creare l'interfaccia del client

Qui sta il cuore di Retrofit: si dichiara un'interfaccia annotata con @RestApi e si descrivono gli endpoint con annotazioni come @GET, @POST, @Path e @Body.

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'user.dart';

part 'api_client.g.dart';

@RestApi(baseUrl: 'https://api.example.com')
abstract class ApiClient {
  factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;

  @GET('/users')
  Future<List<User>> getUsers();

  @GET('/users/{id}')
  Future<User> getUser(@Path('id') int id);

  @POST('/users')
  Future<User> createUser(@Body() User user);

  @GET('/users')
  Future<List<User>> searchUsers(@Query('q') String query);

  @DELETE('/users/{id}')
  Future<void> deleteUser(@Path('id') int id);
}

Generare il codice

Una volta definite interfaccia e modelli, esegui il generatore:

dart run build_runner build --delete-conflicting-outputs

Questo crea i file api_client.g.dart e user.g.dart con tutta l'implementazione ripetitiva. Non dovrai mai scrivere a mano il parsing del JSON o la costruzione degli URL.

Usare il client

void main() async {
  final dio = Dio();
  final client = ApiClient(dio);

  final users = await client.getUsers();
  print('Utenti trovati: ${users.length}');

  final user = await client.getUser(1);
  print('Nome: ${user.name}');

  final nuovo = await client.createUser(
    User(id: 0, name: 'Mario', email: 'mario@example.com'),
  );
  print('Creato con id ${nuovo.id}');
}

Configurare Dio con interceptor

Uno dei vantaggi di appoggiarsi a Dio è poter riutilizzare tutta la sua potenza: interceptor per token di autenticazione, logging e gestione degli errori.

final dio = Dio(BaseOptions(
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 10),
));

dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      options.headers['Authorization'] = 'Bearer $token';
      handler.next(options);
    },
    onError: (error, handler) {
      if (error.response?.statusCode == 401) {
        // logica di refresh del token
      }
      handler.next(error);
    },
  ),
);

final client = ApiClient(dio);

Gestire header e form data

Retrofit supporta anche header dinamici e invio di form:

@POST('/login')
Future<TokenResponse> login(
  @Field('username') String username,
  @Field('password') String password,
);

@GET('/profile')
Future<User> profile(@Header('X-Api-Version') String version);

Annotare i parametri con @Field invia automaticamente i dati come application/x-www-form-urlencoded, mentre @Header permette di passare header specifici a livello di singola chiamata.

Best practice

  • Separa il layer di rete: tieni le interfacce Retrofit in una cartella dedicata (data/remote) e non esporle direttamente alla UI, ma passa attraverso un repository.
  • Gestisci le eccezioni: le DioException vanno intercettate e convertite in errori di dominio (ad esempio con un tipo Result) prima di raggiungere i widget.
  • Un client per dominio: se l'API è ampia, suddividi le chiamate in più interfacce (AuthApi, UserApi, ProductApi) per mantenere il codice ordinato.
  • Rigenera dopo ogni modifica: ricordati di lanciare build_runner ogni volta che modifichi le annotazioni o i modelli.

Conclusioni

Retrofit porta in Flutter un approccio dichiarativo e tipizzato alle chiamate REST, eliminando gran parte del codice boilerplate e riducendo gli errori. Combinato con Dio per il trasporto e json_serializable per la serializzazione, offre un layer di rete robusto e facilmente manutenibile, ideale per progetti di medie e grandi dimensioni.