![005-search](https://i0.wp.com/resocoder.com/wp-content/uploads/2021/01/005-search.png?resize=77%2C77&ssl=1)
Searching through all kinds of various content in your Flutter app is something you'll likely have to implement one day. A successful search implementation needs basically two things - a usable search history and also the search bar widget. Now, we're going to take a look at the most extensible and good-looking way to do this.
The project we'll build
We're going to build a simple project that will contain all of the basic building blocks of a good search implementation. The package that we're going to use for the UI part is called material_floating_search_bar. There are many other packages and even the built-in showSearch function, but the package we're using is one of the best ones out there in terms of customizability and it also looks really good out of the box.
When it comes to the search history part, we're going to keep it simple. We could introduce all kinds of third-party dependencies like different state management packages and all kinds of persistent databases. However, we're going to implement the history only inside a StatefulWidget
and as an in-memory List
. Yet, as you'll see in a little while, the code will be easily applicable to any state management and database solution you like.
If you want to follow along with this tutorial, get the starter project from above which contains a bit of uninteresting yet necessary code already written for you.
Search history & logic
![012-search-engine-3](https://i0.wp.com/resocoder.com/wp-content/uploads/2021/01/012-search-engine-3.png?resize=77%2C77&ssl=1)
It's much simpler to implement the UI when you have the logic and state part out of the way. There are a bunch of things we should be able to do with the search history which are demonstrated in the video above.
- Add a search term and limit the maximum number of terms in history.
- Delete a search term.
- Get filtered search terms based on the user input.
- Reorder the recently searched-for term to be displayed first.
As I've already mentioned, we're going to use an in-memory List
and a StatefulWidget
. No matter which state mangement solution and database you want to use in your project, the principles will remain the same.
Inside the _HomePageState
class, let's create the following fields:
main.dart
static const historyLength = 5;
// The "raw" history that we don't access from the UI, prefilled with values
List<String> _searchHistory = [
'fuchsia',
'flutter',
'widgets',
'resocoder',
];
// The filtered & ordered history that's accessed from the UI
List<String> filteredSearchHistory;
// The currently searched-for term
String selectedTerm;
Filtering terms
![008-search-2](https://i0.wp.com/resocoder.com/wp-content/uploads/2021/01/008-search-2.png?resize=77%2C77&ssl=1)
As the user types in the search term, the history should display only the terms which start with the user input. In other words, typing in "fl" should only show terms such as "flutter" or "flask" because they start with "fl".
filterSearchTerms
will be a function that will take the raw _searchHistory
and output such a filtered list.
main.dart
List<String> filterSearchTerms({
@required String filter,
}) {
if (filter != null && filter.isNotEmpty) {
// Reversed because we want the last added items to appear first in the UI
return _searchHistory.reversed
.where((term) => term.startsWith(filter))
.toList();
} else {
return _searchHistory.reversed.toList();
}
}
List
to store the history data, we also use the filtering function to reverse the order of the list items. This will ensure that the last added terms will be displayed as first.Adding a search term
Adding search terms is not as simple as calling add
on a List
object. We want to ensure there are no duplicate terms in the history. If the term we're trying to add already exists in the history, we instead just want to reorder the already existing term to appear first in the UI.
Also, there's a limit on how many terms the history holds which we've set to 5, so we want to apply the so-called least recently used algorithm and remove the least recently used search term if we go over the history limit.
main.dart
void addSearchTerm(String term) {
if (_searchHistory.contains(term)) {
// This method will be implemented soon
putSearchTermFirst(term);
return;
}
_searchHistory.add(term);
if (_searchHistory.length > historyLength) {
_searchHistory.removeRange(0, _searchHistory.length - historyLength);
}
// Changes in _searchHistory mean that we have to update the filteredSearchHistory
filteredSearchHistory = filterSearchTerms(filter: null);
}
Deleting & reordering
While there's not much to say about deleting a term from history...
main.dart
void deleteSearchTerm(String term) {
_searchHistory.removeWhere((t) => t == term);
filteredSearchHistory = filterSearchTerms(filter: null);
}
Reordering or "putting the search term first" has an interesting implementation. At first we delete the term and then we add it again. Since newly added terms are displayed as first, this method serves its purpose well.
main.dart
void putSearchTermFirst(String term) {
deleteSearchTerm(term);
addSearchTerm(term);
}
String
s in the history. If you'd rather use your own class that's called, let's say, SearchTerm
, feel free to add a timestamp field to it which you can then update and base your sorting upon accordingly.Lastly, let's initialize the filteredSearchHistory
from initState
.
main.dart
@override
void initState() {
super.initState();
filteredSearchHistory = filterSearchTerms(filter: null);
}
Search bar UI
![004-search-engine](https://i0.wp.com/resocoder.com/wp-content/uploads/2021/01/004-search-engine.png?resize=77%2C77&ssl=1)
As you already know, we want to use the material_floating_search_bar package, so let's add it to the pubspec.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
material_floating_search_bar: ^0.2.6
This package comes with a FloatingSearchBar
widget. Unlike the default AppBar
, this widget should be put into the Scaffold
's body
parameter. The content of the actual page, which in this case is the SearchResultsListView
from the starter project, is in turn put into the body
of the FloatingSearchBar
.
main.dart
@override
Widget build(BuildContext context) {
return Scaffold(
body: FloatingSearchBar(
body: SearchResultsListView(
searchTerm: selectedTerm,
),
),
);
}
Since we'll want to programmatically control the search bar, let's also set up the a controller for it.
main.dart
FloatingSearchBarController controller;
@override
void initState() {
super.initState();
controller = FloatingSearchBarController();
filteredSearchHistory = filterSearchTerms(filter: null);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FloatingSearchBar(
controller: controller,
body: SearchResultsListView(
searchTerm: selectedTerm,
),
),
);
}
To enable the commonly desired behavior where the search bar hides when the user scrolls down in a list, we need to wrap the search bar's body in a FloatingSearchBarScrollNotifier
.
main.dart
...
body: FloatingSearchBar(
controller: controller,
body: FloatingSearchBarScrollNotifier(
child: SearchResultsListView(
searchTerm: selectedTerm,
),
),
...
The FloatingSearchBar
also takes in the following cosmetic fields
main.dart
transition: CircularFloatingSearchBarTransition(),
// Bouncing physics for the search history
physics: BouncingScrollPhysics(),
// Title is displayed on an unopened (inactive) search bar
title: Text(
selectedTerm ?? 'The Search App',
style: Theme.of(context).textTheme.headline6,
),
// Hint gets displayed once the search bar is tapped and opened
hint: 'Search and find out...',
To easily clear the string which the user has typed in, there's the searchToClear
action.
main.dart
actions: [
FloatingSearchBarAction.searchToClear(),
],
Callbacks
The first callback function we're interested in is onQueryChanged
that gets triggered when a user types in characters. In our case, we just want to set the filteredSearchHistory
to be filtered with the currently typed-in string.
main.dart
onQueryChanged: (query) {
setState(() {
filteredSearchHistory = filterSearchTerms(filter: query);
});
},
onQueryChanged
such as a network call, consider setting the debounceDelay
so that the callback will not run for every single typed-in character.The second callback is onSubmitted
. Here we want to add and select the submitted query and also programmatically close the search bar.
main.dart
onSubmitted: (query) {
setState(() {
addSearchTerm(query);
selectedTerm = query;
});
controller.close();
},
Displaying (not only) the history
The builder
parameter of the FloatingSearchBar
is what holds the "search history" widget that appears when a user taps on the search bar. We have a completely free hand at how it's styled, so let's begin with making it rounded and white. We're going to use the Material
widget so that the ink splash effect will be visible once we add ListTile
s.
main.dart
builder: (context, transition) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.white,
elevation: 4,
child: Placeholder(
fallbackHeight: 200,
),
),
);
},
We're not always going to display only the filteredSearchHistory
though. If the history is empty and the user has not typed in any search query yet, we want to display a "start searching" message. This means we'll write an if
statement and to run such conditional logic, we'll need to break out our code into another build
method with a Builder
(or create a new separate widget class, but I'm lazy ?).
main.dart
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.white,
elevation: 4,
child: Builder(
builder: (context) {
if (filteredSearchHistory.isEmpty &&
controller.query.isEmpty) {
return Container(
height: 56,
width: double.infinity,
alignment: Alignment.center,
child: Text(
'Start searching',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.caption,
),
);
}
},
),
),
);
Another special case that can occur is when the filtered history is empty and the user has already started typing in the search term. In such a case, we want to display the currently typed-in term in a single ListTile
. Tapping this tile behaves just like the onSubmit
callback we've already implemented.
Continuing in the inner builder
method from above...
main.dart
else if (filteredSearchHistory.isEmpty) {
return ListTile(
title: Text(controller.query),
leading: const Icon(Icons.search),
onTap: () {
setState(() {
addSearchTerm(controller.query);
selectedTerm = controller.query;
});
controller.close();
},
);
}
Lastly, in the else
clause, we want to display the filtered history in a Column
. While we technically could use a ListView
, we'd have to do a bunch of shenaningans. As the package docs say:
builder
is not allowed to have an unbounded (infinite) height. This is necessary in order for the search bar to be able to dismiss itself, when the user taps below the area of the child.Therefore, the easiest way to get a vertical list of widgets is to use a Column
with mainAxisSize
set to be the minimum.
The ListTiles
will have a delete button when the user taps them, the tapped search term will be selected and also put first in the history.
main.dart
else {
return Column(
mainAxisSize: MainAxisSize.min,
children: filteredSearchHistory
.map(
(term) => ListTile(
title: Text(
term,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.history),
trailing: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
deleteSearchTerm(term);
});
},
),
onTap: () {
setState(() {
putSearchTermFirst(term);
selectedTerm = term;
});
controller.close();
},
),
)
.toList(),
);
}
When you run the app now, it works! Searching for a term displays 50 search results in the SearchResultsListView
. Have you noticed something weird though?
Padding the content
The problem is that the first search result is not visible! The count starts from 0, but the only the item number 1 can be seen.
![first_item_not_visible](https://i0.wp.com/resocoder.com/wp-content/uploads/2021/01/first_item_not_visible.png?resize=302%2C537&ssl=1)
It so happens, that the very first ListTile
is perfectly hidden under the search bar but it's there! The solution is to add a padding to the ListView
inside of the SearchResultsListView
widget. It's best not to hardcode any values but instead to add padding based on the true height of the FloatingSearchBar
. We can easily look up the FloatingSearchBarState
that gives us access to the height and and vertical margin values with the familiar of
helper method.
main.dart
class SearchResultsListView extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
...
final fsb = FloatingSearchBar.of(context);
return ListView(
padding: EdgeInsets.only(top: fsb.height + fsb.margins.vertical),
children: List.generate(
50,
(index) => ListTile(
title: Text('$searchTerm search result'),
subtitle: Text(index.toString()),
),
),
);
}
}
Now even the very first search result is visible and you're fully set to use this kind of a search bar setup in your own apps. You should be able to take these methods and easily adapt them to your favorite state management solution and database.
full text search with inverted index
after upgrading to material_floating_search_bar: ^0.3.3
padding: EdgeInsets.only(top: fsb.height + fsb.margins.vertical),
error on the fsb. height and fsb.margins.vertical
Help?
Same here, if you found the solution could you share it?
Hi, you were able to solve that error, can you share the solution please, thanks!
My remote developers are right; the demand for freelance developers is continuously rising as the tech world evolves and businesses become increasingly reliant on technology. A developer’s income depends on the role and the software they are working on. You should check out Eiliana.com; they showcase your skills to the right people.
I must say this is a good post to read. Good job!
Magnificent beat ! I would like to apprentice while you amend your site, how can i subscribe for a blog web site? The account helped me a acceptable deal. I had been a little bit acquainted of this your broadcast offered bright clear idea
I loved as much as you will receive carried out right here. The sketch is attractive, your authored material stylish. nonetheless, you command get got an impatience over that you wish be delivering the following. unwell unquestionably come more formerly again since exactly the same nearly a lot often inside case you shield this hike.
Hi, i think that i saw you visited my web site thus i came to ?eturn the favor텶’m attempting to find things to enhance my site!I suppose its ok to use a few of your ideas!!
Usually I do not read article on blogs, however I would like to say that this write-up very compelled me to take a look at and do it! Your writing style has been amazed me. Thank you, very nice article.
of course like your website but you have to check the spelling on several of your posts. A number of them are rife with spelling issues and I in finding it very troublesome to inform the reality on the other hand I will certainly come back again.
Its like you read my mind! You appear to know so much about this, like you wrote the book in it or something. I think that you can do with a few pics to drive the message home a little bit, but instead of that, this is excellent blog. A fantastic read. I’ll certainly be back.
hello!,I really like your writing so a lot! share we keep up a correspondence extra approximately your post on AOL? I need an expert in this house to unravel my problem. May be that is you! Taking a look ahead to see you.
hi!,I like your writing so much! share we be in contact more approximately your article on AOL? I need a specialist in this area to resolve my problem. Maybe that is you! Looking ahead to see you.
I simply could not go away your web site prior to suggesting that I really enjoyed the standard info a person supply on your guests? Is going to be back incessantly to investigate cross-check new posts.
I have been surfing online more than 3 hours today, yet I never found any interesting article like yours. It is pretty worth enough for me. In my opinion, if all web owners and bloggers made good content as you did, the web will be much more useful than ever before.
Wow, wonderful blog layout! How long have you been blogging for? you make blogging look easy. The overall look of your site is great, as well as the content!
I am not sure where you’re getting your info, but good topic. I needs to spend some time learning much more or understanding more. Thanks for magnificent info I was looking for this information for my mission.
I loved as much as you’ll receive carried out right here. The sketch is attractive, your authored material stylish. nonetheless, you command get bought an nervousness over that you wish be delivering the following. unwell unquestionably come more formerly again as exactly the same nearly a lot often inside case you shield this hike.
Thank you for the auspicious writeup. It in fact was a amusement account it. Look advanced to more added agreeable from you! By the way, how could we communicate?
Thanks, I have just been looking for information about this subject for a long time and yours is the best I’ve discovered till now. However, what in regards to the bottom line? Are you certain in regards to the supply?
Wonderful beat I wish to apprentice while you amend your web site how could i subscribe for a blog web site The account aided me a acceptable deal I had been a little bit acquainted of this your broadcast provided bright clear idea
Wow superb blog layout How long have you been blogging for you make blogging look easy The overall look of your site is magnificent as well as the content
Maximize Water Efficiency with Bwer Pipes’ Irrigation Solutions: At Bwer Pipes, we understand the importance of water conservation in Iraqi agriculture. That’s why our irrigation systems minimize water wastage while delivering precise hydration to your crops. Experience the difference with Bwer Pipes. Visit Bwer Pipes