ShauryaLabs Blog

Flutter tutorial - Bottom Sheet with search view

June 06, 2020

We are back with a new post after a very long time. We hope that every one of you is keeping well in these testing times.

Today, we shall see how to create a BottomSheet with a custom SearchView for your Flutter app.

Go ahead and create a new Flutter project. After the project has been created successfully, your main.dart would look something similar to this:


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      //new
      home: MyHomePage(title: 'Bottom sheet with search view'),
    );
  }
}

The MyHomePage class consists of a RaisedButton which will show a BottomSheet when clicked.

class _MyHomePageState extends State<MyHomePage> {
  List<String> _tempListOfCities;
  //1
  final _scaffoldKey = GlobalKey<ScaffoldState>();
  final TextEditingController textController = new TextEditingController();

  //2
  static List<String> _listOfCities = <String>[
    "Tokyo",
    "New York",
    "London",
    "Paris",
    "Madrid",
    "Dubai",
    "Rome",
    "Barcelona",
    "Cologne",
    "Monte Carlo",
    "Puebla",
    "Florence"
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("Show bottom sheet"),
              onPressed: () {
                _showModal(context);
              },
            ),
          ],
        ),
      ),
    );
  }


    void _showModal(context) {
      showModalBottomSheet(
          isScrollControlled: true,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
          ),
          context: context,
          builder: (context) {
            //3
            return StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
              return DraggableScrollableSheet(
                  expand: false,
                  builder:
                      (BuildContext context, ScrollController scrollController) {
                    return Column(children: <Widget>[
                      Padding(
                          padding: EdgeInsets.all(8),
                          child: Row(children: <Widget>[
                            Expanded(
                                child: TextField(
                                    controller: textController,
                                    decoration: InputDecoration(
                                      contentPadding: EdgeInsets.all(8),
                                      border: new OutlineInputBorder(
                                        borderRadius:
                                            new BorderRadius.circular(15.0),
                                        borderSide: new BorderSide(),
                                      ),
                                      prefixIcon: Icon(Icons.search),
                                    ),
                                    onChanged: (value) {
                                      //4
                                      setState(() {
                                        _tempListOfCities =
                                            _buildSearchList(value);
                                      });
                                    })),
                            IconButton(
                                icon: Icon(Icons.close),
                                color: Color(0xFF1F91E7),
                                onPressed: () {
                                  setState(() {
                                    textController.clear();
                                    _tempListOfCities.clear();
                                  });
                                }),
                          ])),
                      Expanded(
                        child: ListView.separated(
                            controller: scrollController,
                            //5
                            itemCount: (_tempListOfCities != null &&
                                    _tempListOfCities.length > 0)
                                ? _tempListOfCities.length
                                : _listOfCities.length,
                            separatorBuilder: (context, int) {
                              return Divider();
                            },
                            itemBuilder: (context, index) {
                              return InkWell(

                                  //6
                                  child: (_tempListOfCities != null &&
                                          _tempListOfCities.length > 0)
                                      ? _showBottomSheetWithSearch(
                                          index, _tempListOfCities)
                                      : _showBottomSheetWithSearch(
                                          index, _listOfCities),
                                  onTap: () {
                                    //7
                                    _scaffoldKey.currentState.showSnackBar(
                                        SnackBar(
                                            behavior: SnackBarBehavior.floating,
                                            content: Text((_tempListOfCities !=
                                                        null &&
                                                    _tempListOfCities.length > 0)
                                                ? _tempListOfCities[index]
                                                : _listOfCities[index])));

                                    Navigator.of(context).pop();
                                  });
                            }),
                      )
                    ]);
                  });
            });
          });
    }

  //8
  Widget _showBottomSheetWithSearch(int index, List<String> listOfCities) {
    return Text(listOfCities[index],
        style: TextStyle(color: Colors.black, fontSize: 16),textAlign: TextAlign.center);
  }

  //9
  List<String> _buildSearchList(String userSearchTerm) {
    List<String> _searchList = List();

    for (int i = 0; i < _listOfCities.length; i++) {
      String name = _listOfCities[i];
      if (name.toLowerCase().contains(userSearchTerm.toLowerCase())) {
        _searchList.add(_listOfCities[i]);
      }
    }
    return _searchList;
  }
}

We have a few key points to unpack here. So let’s dive in.



  1. We are using a GlobalKey for ScaffoldState for the purposes of showing a SnackBar with the user selected item.
  2. We have created a static list of cities that will be used for this example.
  3. The _showModal() method is calling the showModalBottomSheet() which is returning a StateBuilder as the root Widget here. Now, you may be wondering why?


To quote the official doc:

A platonic widget that both has state and calls a closure to obtain its child widget.

A simpler explanation:

  • Whenever we use BottomSheet in our app, it creates a new Widget within the Navigator stack that isn’t a child of your original widget (MyHomePage in this case).
  • Without using the StatefulBuilder, the BottomSheet would never update with the latest data based on user search term.

The StatefulBuilder is the magic potion here that ties it all up.**

Now that we are clear on that, let’s continue.

  1. We are using the onChanged method within the TextField to track user input and build a _tempListOfCities based on the search term.
  2. The itemCount parameter uses either the _tempListOfCities if there is any search term, or the static _listOfCities. We are doing this so as to make sure the count is always up-to-date.
  3. The child parameter uses the same _tempListOfCities or _listOfCities based on what is available.
  4. Here, we are using the _scaffoldKey within its currentState to show the SnackBar. We are also using the behavior parameter here to make sure that the SnackBar is not blocked by the BottomSheet.
  5. The _showBottomSheetWithSearch simply draws each row item.
  6. The _buildSearchList is the method that performs the actual computation based on user input to generate the _tempListOfCities. For the purposes of this tutorial, the method is simply looping through the entire _listOfCities and comparing each input character with the original list, finding any items that contain the search term.
    In a real world application, there are better approaches than using a for loop for a large dataset. Please keep that in mind.

** There are some other ways of notifying the bottom sheet that a change has occurred besides a StatefulBuilder. You may also use a Listenable to notify the modal that its needs to update. Of course, if you plan to use Listenable make sure that you create a new Stateful or Stateless widget for better organization of your project. Check out ChangeNotifier or ValueNotifier depending on your requirement (if you want the updated value or not).

The final app looks something like this:


custom-sheet-1 custom-sheet-2 custom-sheet-3

For the eagle eyed reader, you may have noticed that we have a DraggableScrollableSheet in the code. We quite like the idea of a DraggableScrollableSheet along with a SearchView that can be swiped up to cover the entire screen.

And that’s it! We managed to add a custom SearchView to our BottomSheet!


We are a mobile first development agency. Check our website or send us an email at hello@shauryalabs.com with your thoughts or questions.

ShauryaLabs

Written by ShauryaLabs.