Flutter Tutorial - How to Implement a Persistent Bottom Sheet

This tutorial will teach you how to implement a persistent bottom sheet in Flutter, which is a sheet that remains visible at the bottom of the screen even when the user scrolls. 

We will use the built-in BottomSheet widget in combination with the Scaffold widget to achieve this.

Material : Persistent Bottom sheet

import 'package:flutter/material.dart';

class BottomSheetFabRoute extends StatefulWidget {

  const BottomSheetFabRoute({super.key});

  @override
  BottomSheetFabRouteState createState() => BottomSheetFabRouteState();
}

class BottomSheetFabRouteState extends State<BottomSheetFabRoute> {
// This variable will hold a reference to the bottom sheet controller.
  late PersistentBottomSheetController sheetController;
// This variable will hold a reference to the scaffold context.
  late BuildContext _scaffoldCtx;
// This boolean variable will determine whether the bottom sheet is currently visible or not.
  bool showSheet = false;

  @override
  Widget build(BuildContext context) {
    // Get the current text theme and apply the surface color to it
    final textTheme = Theme.of(context)
        .textTheme
        .apply(displayColor: Theme.of(context).colorScheme.onSurface);

    // Return a scaffold with a centered text widget that displays a message and a floating action button
    return Scaffold(
      // Use the builder function to get access to the scaffold context
      body: Builder(builder: (BuildContext ctx) {
        _scaffoldCtx = ctx; // Assign the scaffold context to a variable for later use
        return Center(
          child:
          TextStyleExample(name: "Tap button \nbelow", style: textTheme.headlineMedium!.copyWith(color: Theme.of(context).colorScheme.primary)),
        );
      }),
      // Add a floating action button that toggles a bottom sheet
      floatingActionButton: FloatingActionButton(
          heroTag: "fab",
          backgroundColor:Theme.of(context).colorScheme.primary,
          elevation: 3,
          child: Icon(showSheet ? Icons.arrow_downward : Icons.arrow_upward, color: Theme.of(context).colorScheme.onPrimary),
          onPressed: () {
            setState(() {
              showSheet = !showSheet; // Toggle the sheet visibility
              if(showSheet) {
                _showSheet(); // Show the sheet if it's not already showing
              } else {
                Navigator.pop(_scaffoldCtx); // Close the sheet if it's already showing
              }
            });
          }
      ),
    );
  }


  void _showSheet() {
    // get the current text theme with updated display color
    final textTheme = Theme.of(context)
        .textTheme
        .apply(displayColor: Theme.of(context).colorScheme.onSurface);

    // show a bottom sheet and store the returned controller object
    sheetController = showBottomSheet(context: _scaffoldCtx, builder: (BuildContext bc){
      // create a card with elevation and no margin
      return Card(
        elevation: 10,
        margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
        child: Container(
          padding: const EdgeInsets.all(10),
          width: double.infinity,
          color: Theme.of(context).colorScheme.background,


            // add a container to hold the content of the sheet
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Center(
                  child: Container(width: 30, height: 5, decoration:  BoxDecoration(
                    color: Theme.of(context).colorScheme.background,
                    borderRadius: const BorderRadius.all(Radius.circular(5)),
                  )),
                ),
                Container(height: 10),
                Row(
                  children: <Widget>[
                    Container(width: 50),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          TextStyleExample(name: "Dairy milk Chocolate", style: textTheme.titleMedium!.copyWith(color: Theme.of(context).colorScheme.primary)),
                          Container(height: 20),
                          Container(height: 5),
                          const Divider(),
                          Container(
                              padding: const EdgeInsets.symmetric(vertical: 10),
                              child:
                              TextStyleExample(name: "10 min away", style: textTheme.titleMedium!.copyWith(color: Theme.of(context).colorScheme.primary))
                          ),
                          const Divider(),
                        ],
                      ),
                    )
                  ],
                ),
                SizedBox(height: 50,
                  child: Row(
                    children: <Widget>[
                      Container(width: 10), Icon(Icons.location_on, color: Theme.of(context).colorScheme.primary), Container(width: 20),
                      TextStyleExample(name: "740 Valencia St, San Francisco, CA", style: textTheme.titleMedium!)
                    ],
                  ),
                ),
                SizedBox(height: 50,
                  child: Row(
                    children: <Widget>[
                      Container(width: 10), Icon(Icons.phone, color: Theme.of(context).colorScheme.primary), Container(width: 20),
                      TextStyleExample(name: "(415) 349-0942", style: textTheme.titleMedium!)
                    ],
                  ),
                ),
                SizedBox(height: 50,
                  child: Row(
                    children: <Widget>[
                      Container(width: 10), Icon(Icons.location_on, color: Theme.of(context).colorScheme.primary), Container(width: 20),
                      TextStyleExample(name: "741 Valencia St, San Francisco, CA", style: textTheme.titleMedium!)
                    ],
                  ),
                ),
                SizedBox(height: 50,
                  child: Row(
                    children: <Widget>[
                      Container(width: 10), Icon(Icons.phone, color: Theme.of(context).colorScheme.primary), Container(width: 20),
                      TextStyleExample(name: "(416) 349-0942", style: textTheme.titleMedium!)
                    ],
                  ),
                ),
                SizedBox(height: 50,
                  child: Row(
                    children: <Widget>[
                      Container(width: 10),  Icon(Icons.schedule, color: Theme.of(context).colorScheme.primary), Container(width: 20),
                      TextStyleExample(name: "Wed, 10 AM - 9 PM", style: textTheme.titleMedium!)
                    ],
                  ),
                ),
              ],
            )


        ),
      );
    });

    // set a callback function to be called when the bottom sheet is closed
    sheetController.closed.then((value) {
      // update state to indicate that the sheet is no longer being shown
      setState(() {
        showSheet = false;
      });
    });
  }

}


// Typography widget
class TextStyleExample extends StatelessWidget {
  // Constructor for the TextStyleExample class
  const TextStyleExample({
    super.key, // Call the constructor of the superclass
    required this.name, // Declare a required String property called name
    required this.style, // Declare a required TextStyle property called style
  });

  final String name; // Store the name property as a final String
  final TextStyle style; // Store the style property as a final TextStyle

  @override
  Widget build(BuildContext context) {
    return Padding(
      // Set the padding for the widget to 1.0 pixels on all sides
      padding: const EdgeInsets.all(1.0),
      child: Text(
        name, // Display the name property as the text content
        style: style.copyWith(letterSpacing: 1.0), // Apply the style property to the text, with an additional letterSpacing of 1.0
      ),
    );
  }
}

..

  • Import the necessary packages.
  • Create a stateful widget called BottomSheetFabRoute.
  • Override the createState method to return a BottomSheetFabRouteState object.
  • Create a state class called BottomSheetFabRouteState that extends the State class.

  • Declare three variables:

  1. sheetController, a PersistentBottomSheetController that will hold a reference to the bottom sheet controller.
  2. _scaffoldCtx, a BuildContext that will hold a reference to the scaffold context.
  3. showSheet, a boolean that will determine whether the bottom sheet is currently visible or not.


  • Override the build method to return a Scaffold widget with a centered text widget that displays a message and a floating action button.

  • Use the Builder widget to get access to the scaffold context and assign it to the _scaffoldCtx variable for later use.
  • Add a floating action button that toggles a bottom sheet. The button's onPressed method toggles the showSheet boolean and either shows the sheet if it's not already showing or closes the sheet if it's already showing.

floatingActionButton: FloatingActionButton(
  ...
  onPressed: () {
    setState(() {
      showSheet = !showSheet; // Toggle the sheet visibility
      if(showSheet) {
        _showSheet(); // Show the sheet if it's not already showing
      } else {
        Navigator.pop(_scaffoldCtx); // Close the sheet if it's already showing
      }
    });
  }
),

  • Define the _showSheet method that displays the bottom sheet. The method uses the showBottomSheet function to display a card with information about a chocolate store.

void _showSheet() {
  // show a bottom sheet and store the returned controller object
  sheetController = showBottomSheet(context: _scaffoldCtx, builder: (BuildContext bc){
    // create a card with elevation and no margin
    return Card(
      elevation: 10,
      margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
      child: Container(
        padding: const EdgeInsets.all(10),
        width: double.infinity,
        color: Theme.of(context).colorScheme.background,

        // add a container to hold the content of the sheet
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            ...
          ],
        ),
      ),
    );
  });
}

  • Create a custom widget called TextStyleExample that displays a name and a style.

..

Comments