Widget riutilizzabili e componibili in Flutter: crea la tua UI library
Best practiceIntermedio35 min Flutter 3.x

Widget riutilizzabili e componibili in Flutter: crea la tua UI library

Perché creare widget riutilizzabili

Uno degli errori più comuni tra chi inizia con Flutter è duplicare lo stesso codice UI in decine di schermate. Questo rende l'app difficile da mantenere e le modifiche allo stile diventano un incubo.

In questo tutorial impareremo a estrarre i widget, a costruire componenti configurabili tramite parametri, a usare child e builder per la composizione, e a creare una piccola UI library interna coerente. Applicheremo alcune best practice come l'uso di const, la separazione dei componenti e la creazione di widget "presentazionali" indipendenti dalla logica di business.

Alla fine avrai un pulsante primario, una card e un widget di stato vuoto pronti da riutilizzare in tutta l'app.

  1. 1

    Estrarre un widget invece di duplicare il codice

    Il primo passo verso la riutilizzabilità è estrarre in una classe separata un blocco di UI che si ripete. Preferisci sempre creare un StatelessWidget (o StatefulWidget) rispetto a un metodo che ritorna un Widget: la classe è ottimizzata dal framework, supporta i const e viene ricostruita in modo più efficiente.

    Creiamo il file lib/ui/primary_button.dart con un pulsante primario configurabile.

    import 'package:flutter/material.dart';
    
    class PrimaryButton extends StatelessWidget {
      const PrimaryButton({
        super.key,
        required this.label,
        required this.onPressed,
        this.isLoading = false,
        this.icon,
      });
    
      final String label;
      final VoidCallback? onPressed;
      final bool isLoading;
      final IconData? icon;
    
      @override
      Widget build(BuildContext context) {
        return FilledButton(
          onPressed: isLoading ? null : onPressed,
          child: isLoading
              ? const SizedBox(
                  height: 20,
                  width: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    if (icon != null) ...[
                      Icon(icon, size: 18),
                      const SizedBox(width: 8),
                    ],
                    Text(label),
                  ],
                ),
        );
      }
    }

    Risultato atteso

    Hai un widget PrimaryButton che gestisce label, icona opzionale e stato di caricamento, riutilizzabile ovunque.

  2. 2

    Rendere il widget configurabile con parametri e valori di default

    Un buon widget riutilizzabile espone parametri chiari con valori di default sensati. Evita di codificare colori o spaziature fisse: leggi i valori dal Theme così il componente si adatta automaticamente al tema chiaro/scuro.

    Creiamo una AppCard che accetta un child, un padding personalizzabile e un callback opzionale al tap.

    import 'package:flutter/material.dart';
    
    class AppCard extends StatelessWidget {
      const AppCard({
        super.key,
        required this.child,
        this.padding = const EdgeInsets.all(16),
        this.onTap,
      });
    
      final Widget child;
      final EdgeInsetsGeometry padding;
      final VoidCallback? onTap;
    
      @override
      Widget build(BuildContext context) {
        final colors = Theme.of(context).colorScheme;
        return Material(
          color: colors.surfaceContainerHighest,
          borderRadius: BorderRadius.circular(16),
          clipBehavior: Clip.antiAlias,
          child: InkWell(
            onTap: onTap,
            child: Padding(
              padding: padding,
              child: child,
            ),
          ),
        );
      }
    }

    Risultato atteso

    AppCard rispetta il tema dell'app, è cliccabile solo se onTap è fornito e accetta qualsiasi contenuto tramite child.

  3. 3

    Comporre i widget con il pattern child e builder

    La composizione è il cuore di Flutter: invece di ereditare, si combinano widget piccoli e specializzati. Usare una proprietà child (o builder) mantiene i componenti generici e flessibili.

    Creiamo un SectionTile che combina AppCard con un titolo e uno slot per il contenuto. Nota come riutilizziamo AppCard senza duplicarne la logica.

    import 'package:flutter/material.dart';
    import 'app_card.dart';
    
    class SectionTile extends StatelessWidget {
      const SectionTile({
        super.key,
        required this.title,
        required this.child,
        this.trailing,
      });
    
      final String title;
      final Widget child;
      final Widget? trailing;
    
      @override
      Widget build(BuildContext context) {
        final textTheme = Theme.of(context).textTheme;
        return AppCard(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Expanded(
                    child: Text(title, style: textTheme.titleMedium),
                  ),
                  if (trailing != null) trailing!,
                ],
              ),
              const SizedBox(height: 12),
              child,
            ],
          ),
        );
      }
    }

    Risultato atteso

    SectionTile mostra un titolo con un'azione opzionale e il contenuto sottostante, riutilizzando AppCard.

  4. 4

    Creare un widget di stato vuoto riutilizzabile

    Gli stati vuoti (liste senza dati, ricerche senza risultati) si ripetono spesso. Un componente dedicato garantisce coerenza visiva in tutta l'app.

    Creiamo EmptyState con icona, titolo, messaggio e un'azione opzionale che riusa il nostro PrimaryButton.

    import 'package:flutter/material.dart';
    import 'primary_button.dart';
    
    class EmptyState extends StatelessWidget {
      const EmptyState({
        super.key,
        required this.icon,
        required this.title,
        this.message,
        this.actionLabel,
        this.onAction,
      });
    
      final IconData icon;
      final String title;
      final String? message;
      final String? actionLabel;
      final VoidCallback? onAction;
    
      @override
      Widget build(BuildContext context) {
        final theme = Theme.of(context);
        return Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(icon, size: 64, color: theme.colorScheme.outline),
                const SizedBox(height: 16),
                Text(title, style: theme.textTheme.titleLarge, textAlign: TextAlign.center),
                if (message != null) ...[
                  const SizedBox(height: 8),
                  Text(
                    message!,
                    style: theme.textTheme.bodyMedium,
                    textAlign: TextAlign.center,
                  ),
                ],
                if (actionLabel != null && onAction != null) ...[
                  const SizedBox(height: 24),
                  PrimaryButton(label: actionLabel!, onPressed: onAction),
                ],
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Un componente EmptyState pronto da mostrare quando una lista è vuota, con azione facoltativa.

  5. 5

    Centralizzare gli export in un barrel file

    Con più componenti conviene creare un barrel file che raccoglie tutti gli export della tua UI library. Così le import nelle schermate restano pulite: basta un solo import.

    Crea lib/ui/ui.dart.

    // lib/ui/ui.dart
    export 'primary_button.dart';
    export 'app_card.dart';
    export 'section_tile.dart';
    export 'empty_state.dart';

    Risultato atteso

    Puoi importare tutti i componenti con `import 'package:tua_app/ui/ui.dart';`.

  6. 6

    Usare i componenti in una schermata reale

    Mettiamo insieme tutto in una schermata di esempio. Nota come il codice della UI resta leggibile e dichiarativo: nessuna duplicazione di stile, tutto passa dai componenti riutilizzabili.

    import 'package:flutter/material.dart';
    import 'package:tua_app/ui/ui.dart';
    
    class DashboardScreen extends StatelessWidget {
      const DashboardScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        final items = <String>[]; // simuliamo una lista vuota
    
        return Scaffold(
          appBar: AppBar(title: const Text('Dashboard')),
          body: items.isEmpty
              ? EmptyState(
                  icon: Icons.inbox_outlined,
                  title: 'Nessun elemento',
                  message: 'Aggiungi il tuo primo elemento per iniziare.',
                  actionLabel: 'Aggiungi',
                  onAction: () {},
                )
              : ListView(
                  padding: const EdgeInsets.all(16),
                  children: [
                    SectionTile(
                      title: 'Riepilogo',
                      trailing: const Icon(Icons.more_horiz),
                      child: const Text('Contenuto della sezione'),
                    ),
                    const SizedBox(height: 12),
                    PrimaryButton(
                      label: 'Continua',
                      icon: Icons.arrow_forward,
                      onPressed: () {},
                    ),
                  ],
                ),
        );
      }
    }

    Risultato atteso

    La schermata mostra lo stato vuoto con un pulsante 'Aggiungi' coerente con il resto dell'app, dimostrando i componenti in azione.

  7. 7

    Best practice finali per una UI library manutenibile

    Alcune regole pratiche per far crescere la tua libreria di widget senza creare caos:

    • Usa sempre const dove possibile per evitare ricostruzioni inutili.
    • Mantieni i widget presentazionali: nessuna chiamata di rete o logica di business al loro interno. Passa i dati e i callback dall'esterno.
    • Non hardcodare colori e dimensioni: leggi da Theme.of(context) per supportare dark mode e coerenza.
    • Espone parametri opzionali con default sensati invece di creare tante varianti di widget.
    • Documenta i parametri con commenti /// così l'IDE mostra suggerimenti utili.
    • Testa i widget con flutter_test per garantire che le modifiche future non rompano la UI.

    Seguendo questi principi, aggiungere nuove schermate diventa un lavoro di assemblaggio di mattoncini già collaudati.

    Risultato atteso

    Hai una UI library iniziale, coerente e riutilizzabile, e conosci le best practice per estenderla in sicurezza.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!