Bottom sheet persistenti e modali in Flutter con showModalBottomSheet e DraggableScrollableSheet
GuideIntermedio35 min Flutter 3.x

Bottom sheet persistenti e modali in Flutter con showModalBottomSheet e DraggableScrollableSheet

I bottom sheet sono un elemento fondamentale del Material Design: pannelli che scivolano dal basso dello schermo per mostrare azioni contestuali o contenuti aggiuntivi senza cambiare schermata.

In questo tutorial vedremo come usare showModalBottomSheet per i sheet modali, come restituire un valore alla schermata chiamante e come rendere il contenuto trascinabile e scorrevole con DraggableScrollableSheet. Al termine avrai una serie di pattern riutilizzabili per arricchire la UX delle tue app.

  1. 1

    Preparare la schermata di base

    Creiamo una semplice schermata con un pulsante centrale che useremo per aprire i vari tipi di bottom sheet. Partiamo da uno Scaffold minimale.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Bottom Sheet Demo',
          theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Bottom Sheet Demo')),
          body: Center(
            child: ElevatedButton(
              onPressed: () {},
              child: const Text('Apri bottom sheet'),
            ),
          ),
        );
      }
    }

    Risultato atteso

    Una schermata con un pulsante centrale (ancora senza azione) e una AppBar.

  2. 2

    Aprire un bottom sheet modale semplice

    Usiamo showModalBottomSheet per mostrare un pannello modale. Il parametro builder restituisce il contenuto del sheet. Aggiungiamo un padding e un SafeArea per rispettare le zone di sistema.

    Nota il mainAxisSize: MainAxisSize.min sulla Column: fa sì che il sheet occupi solo lo spazio necessario.

    void _mostraSheetSemplice(BuildContext context) {
      showModalBottomSheet(
        context: context,
        builder: (context) {
          return SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  ListTile(
                    leading: const Icon(Icons.share),
                    title: const Text('Condividi'),
                    onTap: () => Navigator.pop(context),
                  ),
                  ListTile(
                    leading: const Icon(Icons.link),
                    title: const Text('Copia link'),
                    onTap: () => Navigator.pop(context),
                  ),
                  ListTile(
                    leading: const Icon(Icons.delete_outline),
                    title: const Text('Elimina'),
                    onTap: () => Navigator.pop(context),
                  ),
                ],
              ),
            ),
          );
        },
      );
    }

    Risultato atteso

    Collegando questa funzione al pulsante, appare dal basso un pannello con tre opzioni cliccabili.

  3. 3

    Restituire un valore alla schermata chiamante

    showModalBottomSheet è asincrono e restituisce un Future con il valore passato a Navigator.pop(context, valore). Questo permette di sapere quale opzione ha scelto l'utente.

    Usiamo await e mostriamo il risultato con uno SnackBar.

    Future<void> _scegliOpzione(BuildContext context) async {
      final scelta = await showModalBottomSheet<String>(
        context: context,
        builder: (context) {
          return SafeArea(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                ListTile(
                  title: const Text('Opzione A'),
                  onTap: () => Navigator.pop(context, 'A'),
                ),
                ListTile(
                  title: const Text('Opzione B'),
                  onTap: () => Navigator.pop(context, 'B'),
                ),
              ],
            ),
          );
        },
      );
    
      if (scelta != null && context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Hai scelto: $scelta')),
        );
      }
    }

    Risultato atteso

    Dopo aver toccato un'opzione, il sheet si chiude e compare uno SnackBar con la scelta effettuata.

  4. 4

    Personalizzare forma, colore e comportamento

    Possiamo personalizzare l'aspetto con shape (angoli arrotondati), backgroundColor e isDismissible. Con isScrollControlled: true permettiamo al sheet di superare metà schermo, utile per form.

    Aggiungiamo anche un piccolo indicatore di trascinamento (grabber) in cima.

    void _mostraSheetPersonalizzato(BuildContext context) {
      showModalBottomSheet(
        context: context,
        isScrollControlled: true,
        backgroundColor: Theme.of(context).colorScheme.surface,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
        ),
        builder: (context) {
          return Padding(
            // Fondamentale per non far coprire i campi dalla tastiera
            padding: EdgeInsets.only(
              bottom: MediaQuery.of(context).viewInsets.bottom,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const SizedBox(height: 12),
                Container(
                  width: 40,
                  height: 4,
                  decoration: BoxDecoration(
                    color: Colors.grey.shade400,
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
                const Padding(
                  padding: EdgeInsets.all(16),
                  child: TextField(
                    decoration: InputDecoration(
                      labelText: 'Scrivi un commento',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      );
    }

    Risultato atteso

    Un bottom sheet con angoli arrotondati, grabber in alto e un campo di testo che si solleva sopra la tastiera.

  5. 5

    Contenuto trascinabile con DraggableScrollableSheet

    Quando il contenuto è lungo e vogliamo che l'utente possa espandere il sheet a schermo intero trascinandolo, usiamo DraggableScrollableSheet.

    I parametri initialChildSize, minChildSize e maxChildSize sono frazioni dell'altezza schermo (da 0 a 1). È essenziale collegare lo scrollController fornito al widget scorrevole interno, altrimenti il trascinamento non funziona correttamente.

    void _mostraSheetTrascinabile(BuildContext context) {
      showModalBottomSheet(
        context: context,
        isScrollControlled: true,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
        ),
        builder: (context) {
          return DraggableScrollableSheet(
            initialChildSize: 0.5,
            minChildSize: 0.25,
            maxChildSize: 0.9,
            expand: false,
            builder: (context, scrollController) {
              return ListView.builder(
                controller: scrollController, // collegamento obbligatorio
                itemCount: 40,
                itemBuilder: (context, index) {
                  if (index == 0) {
                    return const Padding(
                      padding: EdgeInsets.all(16),
                      child: Text(
                        'Trascina per espandere',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    );
                  }
                  return ListTile(
                    leading: CircleAvatar(child: Text('$index')),
                    title: Text('Elemento $index'),
                  );
                },
              );
            },
          );
        },
      );
    }

    Risultato atteso

    Un bottom sheet che parte a metà schermo e può essere trascinato fino al 90% dell'altezza, con lista scorrevole al suo interno.

  6. 6

    Collegare tutto e testare

    Aggiorniamo la HomePage per mostrare tutti i pulsanti collegati alle funzioni create. Trasformiamo il widget in StatefulWidget non è necessario: bastano metodi che ricevono il context.

    Un accorgimento: dopo un await verifica sempre context.mounted prima di usare il BuildContext, per evitare errori se il widget è stato smontato.

    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Bottom Sheet Demo')),
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton(
                  onPressed: () => _mostraSheetSemplice(context),
                  child: const Text('Sheet semplice'),
                ),
                ElevatedButton(
                  onPressed: () => _scegliOpzione(context),
                  child: const Text('Sheet con valore di ritorno'),
                ),
                ElevatedButton(
                  onPressed: () => _mostraSheetPersonalizzato(context),
                  child: const Text('Sheet con campo di testo'),
                ),
                ElevatedButton(
                  onPressed: () => _mostraSheetTrascinabile(context),
                  child: const Text('Sheet trascinabile'),
                ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Quattro pulsanti che aprono rispettivamente i diversi tipi di bottom sheet realizzati durante il tutorial.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!