Gestire gli errori in Flutter con Either e il pacchetto dartz
Best practiceIntermedio35 min Flutter 3.x

Gestire gli errori in Flutter con Either e il pacchetto dartz

Perché gestire gli errori con Either?

In molti progetti Flutter la gestione degli errori si basa su try/catch sparsi ovunque, con il rischio di dimenticare qualche eccezione e far crashare l'app. Un approccio più robusto, ispirato alla programmazione funzionale, consiste nel rendere l'errore esplicito nella firma dei metodi.

Il tipo Either<L, R> del pacchetto dartz rappresenta un valore che può essere di due tipi: Left (per convenzione l'errore) oppure Right (per convenzione il successo). In questo modo il compilatore ti obbliga a gestire entrambi i casi.

In questo tutorial costruiremo un piccolo repository che recupera un utente e restituisce un Either<Failure, User>, mostrando come gestire il risultato nella UI in modo pulito e sicuro.

  1. 1

    Aggiungere le dipendenze

    Aggiungi il pacchetto dartz al tuo progetto. Fornisce il tipo Either e molte altre strutture della programmazione funzionale.

    Esegui il comando da terminale oppure aggiungi manualmente la dipendenza al file pubspec.yaml.

    flutter pub add dartz

    Risultato atteso

    Nel pubspec.yaml comparirà la dipendenza `dartz` e il progetto scaricherà il pacchetto senza errori.

  2. 2

    Definire i tipi di errore (Failure)

    Invece di lanciare eccezioni generiche, definiamo una gerarchia di errori tipizzati. Questo rende chiaro quali errori può restituire ciascuna funzione e facilita la loro gestione nella UI.

    Creiamo una classe base Failure con alcune sottoclassi specifiche.

    abstract class Failure {
      final String message;
      const Failure(this.message);
    }
    
    class ServerFailure extends Failure {
      const ServerFailure([super.message = 'Errore del server']);
    }
    
    class NetworkFailure extends Failure {
      const NetworkFailure([super.message = 'Nessuna connessione']);
    }
    
    class NotFoundFailure extends Failure {
      const NotFoundFailure([super.message = 'Risorsa non trovata']);
    }

    Risultato atteso

    Hai una gerarchia di errori riutilizzabile che rappresenta i possibili fallimenti dell'app.

  3. 3

    Creare il modello User

    Definiamo un semplice modello User che rappresenta il risultato di successo. Includiamo un factory fromJson per simulare la deserializzazione di una risposta REST.

    class User {
      final int id;
      final String name;
      final String email;
    
      const User({required this.id, required this.name, required this.email});
    
      factory User.fromJson(Map<String, dynamic> json) {
        return User(
          id: json['id'] as int,
          name: json['name'] as String,
          email: json['email'] as String,
        );
      }
    }

    Risultato atteso

    Il modello `User` è pronto per essere usato come valore di successo nel tipo Either.

  4. 4

    Implementare il repository che restituisce Either

    Ora creiamo il repository. Invece di lasciar propagare le eccezioni, le catturiamo internamente e le convertiamo in Left(Failure). Il caso di successo viene incapsulato in Right(User).

    Nota la firma del metodo: Future<Either<Failure, User>> comunica chiaramente a chi lo usa che l'operazione può fallire.

    import 'package:dartz/dartz.dart';
    
    class UserRepository {
      Future<Either<Failure, User>> getUser(int id) async {
        try {
          // Simuliamo una chiamata di rete
          await Future.delayed(const Duration(seconds: 1));
    
          if (id <= 0) {
            return const Left(NotFoundFailure('Utente inesistente'));
          }
    
          final json = {
            'id': id,
            'name': 'Mario Rossi',
            'email': 'mario.rossi@example.com',
          };
    
          return Right(User.fromJson(json));
        } on FormatException {
          return const Left(ServerFailure('Risposta non valida'));
        } catch (_) {
          return const Left(NetworkFailure());
        }
      }
    }

    Risultato atteso

    Il repository non lancia più eccezioni verso l'esterno: restituisce sempre un `Either<Failure, User>`.

  5. 5

    Consumare Either con fold

    Il metodo fold di Either prende due funzioni: la prima gestisce il caso Left (errore), la seconda il caso Right (successo). Il compilatore ti costringe a gestire entrambi, quindi non puoi dimenticare l'errore.

    Vediamo un esempio d'uso in una semplice funzione.

    Future<void> caricaUtente() async {
      final repo = UserRepository();
      final risultato = await repo.getUser(1);
    
      risultato.fold(
        (failure) => print('Errore: ${failure.message}'),
        (user) => print('Benvenuto ${user.name}'),
      );
    }

    Risultato atteso

    Chiamando `caricaUtente()` verrà stampato 'Benvenuto Mario Rossi', oppure il messaggio d'errore se qualcosa va storto.

  6. 6

    Mostrare il risultato nella UI

    Integriamo il tutto in un widget. Usiamo un FutureBuilder per attendere il risultato e fold per decidere quale widget mostrare in base a successo o errore.

    Questo pattern rende la UI dichiarativa e completamente sicura rispetto agli errori.

    class UserPage extends StatelessWidget {
      const UserPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        final repo = UserRepository();
    
        return Scaffold(
          appBar: AppBar(title: const Text('Profilo utente')),
          body: FutureBuilder<Either<Failure, User>>(
            future: repo.getUser(1),
            builder: (context, snapshot) {
              if (!snapshot.hasData) {
                return const Center(child: CircularProgressIndicator());
              }
    
              return snapshot.data!.fold(
                (failure) => Center(
                  child: Text(
                    failure.message,
                    style: const TextStyle(color: Colors.red),
                  ),
                ),
                (user) => Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(user.name, style: Theme.of(context).textTheme.headlineSmall),
                      Text(user.email),
                    ],
                  ),
                ),
              );
            },
          ),
        );
      }
    }

    Risultato atteso

    La pagina mostra lo spinner durante il caricamento, poi il nome e l'email dell'utente in caso di successo o un messaggio rosso in caso di errore.

  7. 7

    Trasformare i valori con map e getOrElse

    Either offre metodi comodi per lavorare con i valori senza sempre usare fold.

    • map: trasforma il valore Right mantenendo intatto il Left.
    • getOrElse: restituisce il valore di successo o un default in caso di errore.
    • isRight / isLeft: controlli booleani rapidi.

    Questi helper rendono il codice più conciso quando devi solo trasformare i dati.

    final risultato = await UserRepository().getUser(1);
    
    // Trasformo il User in una stringa, solo se è un Right
    final Either<Failure, String> nomeMaiuscolo =
        risultato.map((user) => user.name.toUpperCase());
    
    // Ottengo un valore di default in caso di errore
    final String nome = risultato
        .map((u) => u.name)
        .getOrElse(() => 'Ospite');
    
    print(nome); // 'Mario Rossi' oppure 'Ospite'

    Risultato atteso

    Puoi trasformare e leggere i valori in modo fluido, gestendo il caso d'errore con un default senza scrivere try/catch.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!