One ‘Hacker’ Article Coming Through!

Automatic Search Bar with Flutter

“전엔 몰랐던 it just comes automatic “ — Red Velvet

Zakiy Saputra

--

This article was written for the purpose of individual assignment for PPL CSUI 2021, and also to educate readers on making an Automatic Search Bar in Flutter!

Prelude

When I was working on the features for my Software Engineering Project, one of the features given asked us to implement a function to show all datasets of a category from a given database in our program. Being an (empathetic? capable? visionary? a very good UX researcher? or just a plain masochist?) programmer, I suggested that the function should also come with a search bar so the client can get specific data with ease. This suggestion gets accepted by the client, and since this function is interesting and because I’m the one who suggested it, I took this feature. In this article, I will give you a grasp on how to make an Automatic Search Bar in Flutter with a given dataset!

Prologue: Why Automatic Search Bar?

To be perfectly honest with you, I actually intended to make a manual search bar (that is, you need to type a whole query before pressing enter) at first, but decided to make an automatic one in the end under few reasons:

  1. The state of Flutter as a framework and Dart as a programming language makes it more feasible for us to create an automatic search bar instead of a manual one. This is because Dart has a function called setState(() {}) that allows us to change a page state for every specific action that is being done; in this example, inserting a char into the search bar.
  2. There’s more documentation of creating an automatic search bar in Flutter compared to a manual one (Actually, I don’t think I’ve seen a manual one yet). These examples are available in the references of this article.
  3. An automatic search bar would give our client more benefits compared to a manual search in this scenario. For example, a client doesn’t have to search with a query of “Lovely Strawberry Shampoo” when they could achieve the same results with fewer taps and typing only “Lovely St” before the result came up. UX-wise, an automatic search bar would be more beneficial.

Under these explanations, I decided to make an automatic search bar. This doesn’t mean that all of your Flutter projects should use an automatic search bar instead of a manual one though. It would be better for you to create a manual search bar with additional buttons as a category filter when your product has a lot of identifiers. Imagine a list of restaurants with categories such as prices, distance, and cuisines, wouldn’t it be better to filter it with a restaurant name search bar accommodated with a filter on each of the restaurant categories?

Preparation

Before we create the search bar, we need to prepare:

A Dataset that is static or achieved from API calls

Since fetching data is a very broad term (depending on your sources of data), I am not going to explain step-by-step how to do this. My best suggestion for you is to do a google search or documentation of the APIs that you use in storing your data. In my case, I gained my dataset from an HTTP call to the internet where I need an authentication token that is generated when I logged in using Firebase. Below is an example code that I used to fetch data from the internet:

The ‘productUri’ that is written in the code snippet above is a variable that contains HTTP strings towards the API, which I won’t share explicitly in this article. Fret not! Below I attached a template that you can use and just need to fill in:

final Uri productUri = Uri.parse('LINKASACCORDINGTOYOURAPI');

Also, even though the dataset needs help from Firebase (as there’s authentication in the process), I didn’t write this article under FlutterFire (❤) since my primary intention is to teach readers on making a search bar for Flutter in general.

Display the Initial Data

After fetching the data and storing it under a variable (in my case, this variable value is Map<String, dynamic>, because there’s a Map inside another Map). You need to write a code that will show each item out of the initial extracted/filtered map you have. On Flutter, this can be achieved by using ListView.builder that is able to support a huge or near-infinite number of lists. Below is the code of a ListView that I write for my program:

Expanded(
child: ListView.builder(
itemCount: length,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: getProportionateScreenHeight(23),
vertical: getProportionateScreenHeight(9),
),
child: Column(
children: [
ListTile(
leading: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 64,
minHeight: 64,
maxWidth: 64,
maxHeight: 64,
),
child: getImage(
map['products'][index]['image'])),
isThreeLine: true, // from map, in my example, our data set contains images

title: Text(
map['products'][index]['name'],
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: blueSecondary,
),
),
subtitle: Column(
children: [
SizedBox(
width: double.infinity,
child: Text(
getCategory(map['products'][index]
['category']), //from map

style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: greyPrimary,
),
),
),
SizedBox(
width: double.infinity,
child: Text(
'Stok: ${map['products'][index]['stock'].toString()}', // from map
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: greyPrimary,
),
),
),
]),
),
ButtonBar(
children: <Widget>[
SizedBox(
width: getProportionateScreenWidth(130),
child: ElevatedButton(
child: const Text('Tambah'),
style: ElevatedButton.styleFrom(
primary: bluePrimary
),
onPressed: () {
// Create new page for more details of said element in list, transferring desired data from element
},
),
),
SizedBox(
width: getProportionateScreenWidth(130),
child: ElevatedButton(
child: const Text('Atur'),
style: ElevatedButton.styleFrom(
primary: blueSecondary
),
onPressed: () {
// Create new page for more details of said element in list, transferring desired data from element
},
),
)
],
),
],
),
),
);
}),
)

This code would then be iterated for each element of the list inside the map we give the widget, showing us something equal to this:

The Making of Automatic Search Bar

Creating The Search Bar

Our Automatic Flutter search bar can be written by using this code:

Container(
constraints: BoxConstraints.expand(
width: getProportionateScreenWidth(360),
height: getProportionateScreenWidth(80),
),
decoration: const BoxDecoration(color: white),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15, horizontal: 20),
child: TextField(
key: const Key('filter'),
inputFormatters: [
FilteringTextInputFormatter.singleLineFormatter
],
keyboardType: TextInputType.text,
onChanged: (value) {
setState(() {
searchString = value;
});
},

controller: _searchController,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15.0),
borderSide: const BorderSide(
color: bluePrimary,
width: 2.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15.0),
borderSide: const BorderSide(
color: blueSecondary,
width: 2.0,
),
),
prefixIcon: const Icon(Icons.search),
labelText: 'eg: Sabun',
),
),
)),

This will result in the following picture:

The important code snippet that we need to pay attention to here is the onChanged. In this scenario, onChanged will always be run everytime input character is added (or deleted) in our TextField. In our scenario, every time our input on TextField is changed, setState will change our searchString value and update our page UI. Since our UI displays elements of Map that is updated by search logic run every setState(), our program will update the results for every input we write, which results in an Automatic Flutter Search Bar.

Note:
Don’t forget to define searchString in your class beforehand!

Creating our Filter Logic

In this code snippet, I showed you a glimpse of how our filter logic works. Few things to be taken note of here. the upper part of the code that I bolded explains the data preprocessing that I gained from my API. I used length var to limit the number of data that can be seen from the list of products and filter (you can change it to however you want or even remove it!). When searchString != ‘’, we do our search logic where I created a new filteredMap to filter matching elements from our original map (map) based on their product name and category. Every time the result matches, we will add the element into our filteredMap to be given later. In my case, the product name is my product name such as ‘Sabun Cair 1 L’ and the category is a list consisting of ‘cleaning’ and ‘liquid’.

body: FutureBuilder(
future: futureProduct,
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.hasData);
final datas = snapshot.data; // data preprocessing

Map<String, dynamic> map = datas.getData();
int length = 10; // In this example, I only show 10 data at most
if (searchString == '') {
if (map['products'].length < 10) {
length = map['products'].length;
}
} else { // do search
final filteredMap = <String, dynamic>{};
final List<dynamic> filteredList = [];
filteredMap['products'] = filteredList;
for (int i = 0; i < map['products'].length; i++) {
if (map['products'][i]['name']
.toLowerCase()
.contains(searchString.toLowerCase())) //search name
{
filteredMap['products'].add(map['products'][i]);
} else {
for (int j = 0;
j < map['products'][i]['category'].length;
j++) {
if (map['products'][i]['category'][j]
.toLowerCase()
.contains(searchString.toLowerCase())) //search category
{
filteredMap['products'].add(map['products'][i]);
}
}
}
}

What if The Search Bar Yields no Result?

It is important for our program to yield feedback towards the user whatever input our user gives. In the case of no matching data when doing a search, our program must show a ‘no data’ message to users instead of showing blank results. To achieve this, I wrote the following code after our program did a search from our initial map to check if our filtered map is empty or not. When our filtered map is empty, our program will show a message to the user that there’s no matching result. If there’s data, show element inside our map as per usual.

if (filteredMap['products'].isEmpty) {
return Column(children: [
// Reinsert our code for our search bar in previous section
Padding(
padding: const EdgeInsets.symmetric(
vertical: 145, horizontal: 20),
child: Text('Hasil Pencarian Anda Kosong',
style: TextStyle(
fontSize: getProportionateScreenWidth(18),
fontWeight: FontWeight.bold,
)))
]);
}

When run, our program will look like this:

When our filteredMap is not empty, set map as filteredMap and show the results from our new updated map.

} else {
map = filteredMap;
if (map['products'].length < 10) {
length = filteredMap['products'].length;
}
}

--

--