Cosa sono le estensioni in Dart

Gli extension methods sono una funzionalità di Dart (disponibile dalla versione 2.7) che consente di aggiungere metodi, getter, setter e operatori a classi già esistenti, anche a quelle che non possiamo modificare come String, int, List o le classi del framework Flutter come BuildContext.

Sono particolarmente utili in Flutter per ridurre il codice ripetitivo (boilerplate) e migliorare la leggibilità, mantenendo una sintassi fluente e naturale.

Sintassi di base

Una estensione si dichiara con la parola chiave extension, seguita opzionalmente da un nome, dal tipo che si vuole estendere e dal corpo:

extension StringExtensions on String {
  bool get isValidEmail {
    final regex = RegExp(r'^[\w.-]+@[\w-]+\.[\w.-]+\$');
    return regex.hasMatch(this);
  }

  String capitalize() {
    if (isEmpty) return this;
    return '\${this[0].toUpperCase()}\${substring(1)}';
  }
}

Una volta importata l'estensione, i metodi si utilizzano come se fossero nativi del tipo:

final email = 'utente@esempio.it';
print(email.isValidEmail); // true
print('flutter'.capitalize()); // Flutter

Dentro l'estensione, this fa riferimento all'istanza su cui il metodo viene chiamato.

Estendere BuildContext: il caso d'uso più comune

Uno degli usi più diffusi in Flutter è estendere BuildContext per accorciare l'accesso a Theme, MediaQuery e alla navigazione:

extension ContextExtensions on BuildContext {
  ThemeData get theme => Theme.of(this);
  TextTheme get textTheme => Theme.of(this).textTheme;
  ColorScheme get colors => Theme.of(this).colorScheme;

  Size get screenSize => MediaQuery.sizeOf(this);
  double get screenWidth => MediaQuery.sizeOf(this).width;
  double get screenHeight => MediaQuery.sizeOf(this).height;

  bool get isDarkMode => Theme.of(this).brightness == Brightness.dark;
}

Il risultato è un codice molto più conciso all'interno dei widget:

@override
Widget build(BuildContext context) {
  return Text(
    'Ciao Flutter',
    style: context.textTheme.headlineMedium?.copyWith(
      color: context.colors.primary,
    ),
  );
}

Estensioni per liste e widget

Un pattern utile è aggiungere spaziatura tra i figli di una Column o Row:

extension WidgetListExtensions on List<Widget> {
  List<Widget> separatedBy(Widget separator) {
    if (isEmpty) return this;
    return [
      for (var i = 0; i < length; i++) ...[
        this[i],
        if (i < length - 1) separator,
      ],
    ];
  }
}

Utilizzo:

Column(
  children: [
    Text('Primo'),
    Text('Secondo'),
    Text('Terzo'),
  ].separatedBy(const SizedBox(height: 12)),
)

Possiamo anche estendere direttamente i widget per applicare modificatori in stile fluente:

extension PaddingExtension on Widget {
  Widget paddingAll(double value) =>
      Padding(padding: EdgeInsets.all(value), child: this);

  Widget get expanded => Expanded(child: this);

  Widget center() => Center(child: this);
}
Text('In evidenza').paddingAll(16).center();

Estensioni generiche e con vincoli

Le estensioni supportano i generics e i vincoli di tipo, utili per operare su collezioni tipizzate:

extension IterableExtensions<T> on Iterable<T> {
  T? get firstOrNull => isEmpty ? null : first;

  Iterable<T> separated(T separator) sync* {
    final iterator = this.iterator;
    if (!iterator.moveNext()) return;
    yield iterator.current;
    while (iterator.moveNext()) {
      yield separator;
      yield iterator.current;
    }
  }
}

Extension types: la novità di Dart 3

Con Dart 3 sono arrivati gli extension types, che permettono di creare un wrapper a costo zero attorno a un tipo esistente, offrendo type safety senza overhead a runtime:

extension type UserId(int value) {
  bool get isValid => value > 0;
}

void fetchUser(UserId id) {
  // id è distinto da un normale int a compile time
}

A differenza delle estensioni classiche, un extension type crea un nuovo tipo statico: questo evita ad esempio di confondere un UserId con un ProductId pur essendo entrambi int.

Best practice e limiti

  • Un solo import esplicito: le estensioni devono essere importate per essere visibili; se due estensioni definiscono lo stesso metodo, si genera un conflitto risolvibile con ExtensionName(oggetto).metodo().
  • Non sovrascrivono i membri esistenti: se un tipo ha già un metodo con lo stesso nome, quello nativo ha la precedenza.
  • Non possono aggiungere campi di istanza: solo metodi, getter, setter e operatori.
  • Attenzione all'abuso: raggruppa le estensioni in file dedicati (es. context_extensions.dart) e non nasconderci logica di business complessa.

Conclusione

Gli extension methods sono uno strumento semplice ma potentissimo per rendere il codice Flutter più espressivo e ridurre la ripetizione. Usati con moderazione e ben organizzati, migliorano notevolmente la leggibilità di un progetto. Con l'arrivo degli extension types di Dart 3, il pattern si arricchisce anche di garanzie di type safety a costo zero.