Estrarre codice riutilizzabile in Flutter con le extension methods di Dart
Best practiceIntermedio25 min Flutter 3.x

Estrarre codice riutilizzabile in Flutter con le extension methods di Dart

Le extension methods sono una delle funzionalità più potenti e sottovalutate di Dart. Permettono di aggiungere nuovi metodi e proprietà a classi già esistenti — anche a quelle della libreria standard o di package di terze parti — senza modificarne il codice sorgente né ricorrere all'ereditarietà.

In questo tutorial vedremo come usarle in un progetto Flutter reale per ridurre il boilerplate: aggiungeremo scorciatoie a BuildContext per accedere a tema e dimensioni, arricchiremo String con validazioni utili e formatteremo le date con DateTime. Alla fine avrai un piccolo insieme di extension pronte da riusare in tutti i tuoi progetti.

  1. 1

    Capire cos'è una extension method

    Una extension aggiunge funzionalità a un tipo esistente senza sottoclassarlo. La sintassi di base è:

    extension NomeExtension on TipoEsistente {
      // metodi e getter
    }
    

    Il NomeExtension è opzionale ma consigliato: serve per risolvere eventuali conflitti e per importare selettivamente l'extension. Dentro il corpo puoi usare la parola chiave this per riferirti all'istanza del tipo esteso.

    Iniziamo con un esempio semplice su int per prendere confidenza con la sintassi.

    extension IntUtils on int {
      bool get isEven => this % 2 == 0;
    
      Duration get seconds => Duration(seconds: this);
    
      int doubled() => this * 2;
    }
    
    void main() {
      print(4.isEven);   // true
      print(3.doubled()); // 6
      print(5.seconds);   // 0:00:05.000000
    }

    Risultato atteso

    Il codice compila ed esegue stampando true, 6 e la durata di 5 secondi, dimostrando che i metodi sono ora disponibili direttamente sugli interi.

  2. 2

    Creare scorciatoie utili su BuildContext

    In Flutter accediamo spessissimo a tema, colori e dimensioni tramite Theme.of(context) e MediaQuery.of(context). Sono chiamate lunghe e ripetitive. Con una extension su BuildContext possiamo creare delle scorciatoie leggibili.

    Crea un file lib/extensions/context_extensions.dart e definisci l'extension seguente.

    import 'package:flutter/material.dart';
    
    extension ContextExtensions on BuildContext {
      // Tema
      ThemeData get theme => Theme.of(this);
      TextTheme get textTheme => theme.textTheme;
      ColorScheme get colors => theme.colorScheme;
    
      // Dimensioni schermo
      Size get screenSize => MediaQuery.sizeOf(this);
      double get screenWidth => screenSize.width;
      double get screenHeight => screenSize.height;
      bool get isTablet => screenWidth >= 600;
    
      // Navigazione rapida
      void pop<T>([T? result]) => Navigator.of(this).pop(result);
    
      Future<T?> push<T>(Widget page) =>
          Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
    }

    Risultato atteso

    Il file compila senza errori ed esporta un'extension con getter per tema, colori e dimensioni oltre a metodi di navigazione.

  3. 3

    Usare l'extension di BuildContext in un widget

    Vediamo la differenza in un widget reale. Importa il file creato e usa le nuove scorciatoie: il codice diventa molto più compatto e leggibile.

    import 'package:flutter/material.dart';
    import 'extensions/context_extensions.dart';
    
    class ProfileHeader extends StatelessWidget {
      const ProfileHeader({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          width: context.screenWidth,
          padding: const EdgeInsets.all(16),
          color: context.colors.primaryContainer,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Benvenuto',
                style: context.textTheme.headlineSmall,
              ),
              if (context.isTablet)
                Text('Layout tablet attivo', style: context.textTheme.bodySmall),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Il widget mostra un header colorato con il colore del tema e adatta il testo mostrato in base alla larghezza dello schermo, senza chiamate verbose a Theme.of e MediaQuery.of.

  4. 4

    Arricchire String con validazioni e formattazioni

    Le validazioni di form e la manipolazione di testo sono operazioni comuni. Con un'extension su String (e String? per gestire i valori nullable) centralizziamo la logica in un unico punto.

    Crea lib/extensions/string_extensions.dart.

    extension StringExtensions on String {
      bool get isValidEmail {
        final regex = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.-]+$');
        return regex.hasMatch(this);
      }
    
      String get capitalized {
        if (isEmpty) return this;
        return '${this[0].toUpperCase()}${substring(1)}';
      }
    
      String truncate(int maxLength) {
        if (length <= maxLength) return this;
        return '${substring(0, maxLength)}\u2026';
      }
    }
    
    extension NullableStringExtensions on String? {
      bool get isNullOrBlank => this == null || this!.trim().isEmpty;
    }

    Risultato atteso

    Ottieni metodi come 'mario@test.it'.isValidEmail (true), 'ciao'.capitalized ('Ciao') e il controllo isNullOrBlank su stringhe nullable.

  5. 5

    Formattare le date con un'extension su DateTime

    Formattare date è ripetitivo: usiamo il package intl incapsulandolo dentro extension dedicate, così da non dover ricreare i DateFormat ovunque.

    Aggiungi la dipendenza con:

    flutter pub add intl
    

    Poi crea lib/extensions/date_extensions.dart.

    import 'package:intl/intl.dart';
    
    extension DateTimeExtensions on DateTime {
      String get shortDate => DateFormat('dd/MM/yyyy').format(this);
    
      String get readable => DateFormat('d MMMM yyyy', 'it_IT').format(this);
    
      bool get isToday {
        final now = DateTime.now();
        return year == now.year && month == now.month && day == now.day;
      }
    
      String get relative {
        final diff = DateTime.now().difference(this);
        if (diff.inMinutes < 1) return 'adesso';
        if (diff.inHours < 1) return '${diff.inMinutes} min fa';
        if (diff.inDays < 1) return '${diff.inHours} ore fa';
        return shortDate;
      }
    }

    Risultato atteso

    Puoi scrivere DateTime.now().shortDate per ottenere '05/06/2024' o .relative per '3 min fa', semplificando la formattazione delle date in tutta l'app.

  6. 6

    Organizzare le extension con un file barrel

    Per evitare import multipli in ogni file, raggruppa tutte le tue extension in un unico barrel file. Così importi una sola riga e hai accesso a tutto il tuo toolkit.

    Crea lib/extensions/extensions.dart.

    // lib/extensions/extensions.dart
    export 'context_extensions.dart';
    export 'string_extensions.dart';
    export 'date_extensions.dart';
    
    // In qualsiasi widget:
    // import 'package:mia_app/extensions/extensions.dart';
    //
    // Text(user.name.capitalized, style: context.textTheme.titleLarge)
    // Text(order.date.relative)
    // if (email.isValidEmail) { ... }

    Risultato atteso

    Con un solo import hai a disposizione tutte le extension, mantenendo il codice dei widget pulito e conciso.

  7. 7

    Best practice e limiti da conoscere

    Le extension sono potenti ma vanno usate con criterio. Ecco alcune linee guida:

    • Non abusarne: crea extension solo per logica genuinamente riutilizzabile, non per nascondere complessità di dominio.
    • Sono risolte staticamente: il metodo chiamato dipende dal tipo statico della variabile, non da quello runtime. Un'extension su Animal non verrà applicata se la variabile è tipizzata come dynamic.
    • Nessun override: se il tipo ha già un metodo con lo stesso nome, vince sempre il metodo originale della classe.
    • Conflitti: se due extension importate definiscono lo stesso metodo, usa NomeExtension(oggetto).metodo() per disambiguare.
    • Nessuno stato: le extension non possono dichiarare campi di istanza, solo metodi e getter/setter.

    Seguendo queste regole otterrai un codice più espressivo senza sorprese.

    // Disambiguare due extension in conflitto
    // StringExtensions('ciao').capitalized;
    
    // Attenzione al tipo statico:
    dynamic value = 4;
    // value.doubled(); // ERRORE: extension non applicata su dynamic
    int number = 4;
    number.doubled(); // OK

    Risultato atteso

    Comprendi quando e come usare le extension in modo sicuro, evitando i due errori più comuni: l'uso su dynamic e i conflitti tra nomi.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!