Perché la composizione è la chiave in Flutter

Uno dei principi fondamentali di Flutter è "composition over inheritance": invece di estendere classi per aggiungere comportamenti, si combinano widget piccoli e focalizzati per costruirne di più complessi. Adottare questo approccio in modo consapevole porta a UI più leggibili, testabili e riutilizzabili.

In questo articolo vedremo tecniche concrete per progettare widget realmente riutilizzabili, evitando gli errori più comuni che rendono il codice rigido e difficile da mantenere.

Il problema dei widget monolitici

Capita spesso di creare un widget che fa troppe cose: gestisce lo stato, definisce lo stile, contiene logica di business e magari anche chiamate di rete. Il risultato è un componente impossibile da riutilizzare in un contesto diverso.

La soluzione è scomporre il widget in parti più piccole, ognuna con una singola responsabilità, e ricomporle.

// Widget poco riutilizzabile: stile e contenuto sono fusi insieme
class PromoCard extends StatelessWidget {
  const PromoCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.blue.shade50,
        borderRadius: BorderRadius.circular(12),
      ),
      child: const Text('Offerta speciale del giorno!'),
    );
  }
}

Parametrizzare con i widget come proprietà

Un modo potente per rendere flessibile un componente è passare altri widget come parametri. Questo è esattamente ciò che fanno molti widget del framework (pensa a Scaffold con appBar, body, floatingActionButton).

class AppCard extends StatelessWidget {
  const AppCard({
    super.key,
    required this.child,
    this.leading,
    this.trailing,
    this.onTap,
  });

  final Widget child;
  final Widget? leading;
  final Widget? trailing;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Theme.of(context).colorScheme.surfaceContainer,
      borderRadius: BorderRadius.circular(12),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              if (leading != null) ...[leading!, const SizedBox(width: 12)],
              Expanded(child: child),
              if (trailing != null) ...[const SizedBox(width: 12), trailing!],
            ],
          ),
        ),
      ),
    );
  }
}

Questo AppCard definisce la struttura e lo stile, ma lascia al chiamante la libertà di decidere il contenuto. Gli slot opzionali (leading, trailing) rendono il componente adattabile a molti casi d'uso.

Il pattern builder per contenuti dinamici

Quando il contenuto dipende da uno stato interno del widget, passare un widget statico non basta. In questi casi si usa il builder pattern: una funzione che riceve dati e restituisce un widget.

class ExpandablePanel extends StatefulWidget {
  const ExpandablePanel({
    super.key,
    required this.header,
    required this.bodyBuilder,
  });

  final Widget header;
  final Widget Function(BuildContext context, bool isExpanded) bodyBuilder;

  @override
  State<ExpandablePanel> createState() => _ExpandablePanelState();
}

class _ExpandablePanelState extends State<ExpandablePanel> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        InkWell(
          onTap: () => setState(() => _expanded = !_expanded),
          child: widget.header,
        ),
        AnimatedCrossFade(
          duration: const Duration(milliseconds: 250),
          crossFadeState:
              _expanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
          firstChild: const SizedBox(width: double.infinity),
          secondChild: widget.bodyBuilder(context, _expanded),
        ),
      ],
    );
  }
}

Il builder consente di esporre uno stato interno (isExpanded) senza costringere il chiamante a gestirlo direttamente.

Estrarre logica con i mixin (quando ha senso)

La composizione non riguarda solo la UI. Per comportamenti trasversali come i controller di animazione, i mixin restano uno strumento valido:

class PulseButton extends StatefulWidget {
  const PulseButton({super.key, required this.child, this.onPressed});

  final Widget child;
  final VoidCallback? onPressed;

  @override
  State<PulseButton> createState() => _PulseButtonState();
}

class _PulseButtonState extends State<PulseButton>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 120),
    lowerBound: 0.95,
    upperBound: 1.0,
    value: 1.0,
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => _controller.reverse(),
      onTapUp: (_) {
        _controller.forward();
        widget.onPressed?.call();
      },
      onTapCancel: () => _controller.forward(),
      child: ScaleTransition(scale: _controller, child: widget.child),
    );
  }
}

Best practice per widget riutilizzabili

  • Una responsabilità per widget: separa presentazione, layout e logica.
  • Preferisci const costruttori: migliorano le performance evitando ricostruzioni inutili.
  • Esponi API minimali ma flessibili: usa parametri opzionali e valori di default sensati.
  • Evita dipendenze nascoste: un widget riutilizzabile non dovrebbe conoscere lo stato globale dell'app; ricevi i dati tramite parametri o callback.
  • Documenta gli slot: chiarisci cosa ci si aspetta in child, leading, ecc.
  • Non abusare dell'ereditarietà: estendere StatelessWidget o StatefulWidget va bene, ma evita gerarchie profonde di widget custom.

Conclusione

Progettare con la composizione richiede un piccolo cambio di mentalità, ma ripaga con codice più pulito, componenti facilmente testabili e una UI che cresce senza diventare ingestibile. Slot, builder e widget parametrizzati sono gli strumenti che Flutter stesso usa: sfruttarli nel proprio codice significa scrivere widget davvero riutilizzabili.