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 {
  List _tempListOfCities;
  //1
  final _scaffoldKey = GlobalKey();
  final TextEditingController textController = new TextEditingController();

  //2
  static List _listOfCities = [
    "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: [
            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: [
                      Padding(
                          padding: EdgeInsets.all(8),
                          child: Row(children: [
                            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 listOfCities) {
    return Text(listOfCities[index],
        style: TextStyle(color: Colors.black, fontSize: 16),textAlign: TextAlign.center);
  }

  //9
  List _buildSearchList(String userSearchTerm) {
    List _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.

https://media.giphy.com/media/YTPVFEsHlVyDBIW24g/giphy.gif

  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 theshowModalBottomSheet()which is returning a StateBuilder as the root Widget here. Now, you may be wondering why?

https://media.giphy.com/media/1060QCIa82ApgY/giphy.gif

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 newWidgetwithin the Navigator stack that isn’t a child of your original widget (MyHomePagein 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_listOfCitiesbased 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 theBottomSheet.
  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:

https://media.giphy.com/media/d2sgieKz9D2GOiWUB4/giphy.gif

https://media.giphy.com/media/aMrP1xpLAQvnPYhAIL/giphy.gif

https://media.giphy.com/media/nQIDirHhsEBffezlgk/giphy.gif

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 Flutter app development agency. Check our website or send us an email at hello@oaktreeapps.com with your thoughts or questions.

0