Jun 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 {
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.
- We are using a
GlobalKey
forScaffoldState
for the purposes of showing aSnackBar
with the user selected item. - We have created a static list of cities that will be used for this example.
- The
_showModal()
method is calling theshowModalBottomSheet()
which is returning aStateBuilder
as the rootWidget
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 newWidget
within theNavigator
stack that isn’t a child of your original widget (MyHomePage
in this case). - Without using the
StatefulBuilder
, theBottomSheet
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.
- We are using the
onChanged
method within theTextField
to track user input and build a_tempListOfCities
based on the search term. - 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. - The
child
parameter uses the same_tempListOfCities
or_listOfCities
based on what is available. - Here, we are using the
_scaffoldKey
within itscurrentState
to show theSnackBar
. We are also using thebehavior
parameter here to make sure that theSnackBar
is not blocked by theBottomSheet
. - The
_showBottomSheetWithSearch
simply draws each row item. - 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:
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.