Making Flutter and REST API Work Together — Optimizing Search (Part 9)

Alex Josef Bigler
Full Struggle Developer
6 min readApr 23, 2023

In today’s fast-paced world, having a quick and efficient search function in your app can make all the difference in terms of user experience. In this article, we will explore how to add a search feature to your Flutter app that uses a REST API to fetch data.

We will start by modifying the existing code for a list of albums, adding a search bar to allow users to search for albums by keyword. We will also look at optimizing the search function to reduce the number of API requests and improve the app’s performance.

By the end of this article, you will have a working search feature that seamlessly integrates with a REST API and delivers quick and efficient search results to your users. So, let’s get started!

The following posts are suggested reading for this post to get you started:

For our work, we will be using a custom fake API based on json-server. It contains a sufficient amount of data for debugging purposes, as well as resources that are interrelated, which we can add at any time if we need something new.

If you want to deploy such a server yourself, you can read my article here 👈.

So

Let’s add a search by album name to our list of albums. We will use the GET /albums?q=name request to retrieve a list of albums with the specified word in the name.

First, create a TextEditingController field in the _AlbumListWidgetState state:

TextEditingController _searchController = TextEditingController();

Next, let’s add a TextFormField to the AppBar for entering search queries:

appBar: AppBar(
title: TextFormField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search albums",
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
),
onFieldSubmitted: (value) {},
),
),

Now we need to modify the _fetchAlbums() function to use the search query:

_fetchAlbums() async {
setState(() {
_isLoading = true;
});
List<Album> newAlbums;
if (_searchController.text.isNotEmpty) {
newAlbums = await _apiProvider.searchAlbums(_searchController.text);
} else {
newAlbums = await _apiProvider.fetchAlbums(_currentPage, _pageSize);
}
await _fetchArtists(newAlbums);

setState(() {
_isLoading = false;
_albums.addAll(newAlbums);
_currentPage++;
});
}

Then create a _clearAlbums() method to clear the list of albums:

void _clearAlbums() {
setState(() {
_albums.clear();
_currentPage = 1;
});
}

Update the _fetchAlbums() method to call the _clearAlbums() method when the search form is submitted:

_fetchAlbums() async {
setState(() {
_isLoading = true;
});
List<Album> newAlbums;
if (_searchController.text.isNotEmpty) {
_clearAlbums();
newAlbums = await _apiProvider.searchAlbums(_searchController.text);
} else {
newAlbums = await _apiProvider.fetchAlbums(_currentPage, _pageSize);
}
await _fetchArtists(newAlbums);

setState(() {
_isLoading = false;
_albums.addAll(newAlbums);
_currentPage++;
});
}

Now let’s step back from the widget with the list of albums and add a searchAlbums() method to the ApiProvider class to perform a search request for albums:

Future<List<Album>> searchAlbums(String query) async {
final response = await http.get(Uri.parse("$_baseUrl/albums?q=$query"));

final responseBody = processResponse(response);

List jsonResponse = json.decode(responseBody);
return jsonResponse.map((album) => Album.fromJson(album)).toList();
}

Now you can search for albums by name by entering a search query into the text field and clicking the search button. When the search form is submitted, the list of albums will be cleared and a new list will be loaded using the search query.

It look like this:

It seems to be working, but this is bullshit and cannot be left as it is. Therefore, we continue.

C’mon, let’s go

We are starting to refactor the methods, and the first one on the list is _fetchAlbums:

_fetchAlbums([String? query]) async {
setState(() {
_isLoading = true;
});

List<Album> newAlbums;
if (query?.isNotEmpty == true) {
_clearAlbums();
newAlbums = await _apiProvider.searchAlbums(query!);
} else {
newAlbums = await _apiProvider.fetchAlbums(_currentPage, _pageSize);
}

await _fetchArtists(newAlbums);

setState(() {
_isLoading = false;
_albums.addAll(newAlbums);
_currentPage++;
});
}

In the new method _fetchAlbums([String? query]), we pass a query parameter that is optional and can be null. If query has a value, we perform a search for albums, otherwise, we fetch all albums. The previous method _fetchAlbums(), uses the search controller _searchController to get the query value and passes it to the searchAlbums() method to perform a search for albums.

Furthermore, in the new method, we do not handle user input directly, and the query is passed only when the method is called with an argument, whereas in the previous method, we use TextEditingController, which reacts to text changes in real-time.

Next, we’ll refactor the TextFormField :

TextFormField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search albums",
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: () {
_clearAlbums();
_fetchAlbums(_searchController.text);
},
),
),
onFieldSubmitted: (value) {
_clearAlbums();
_fetchAlbums(value);
},
),

Let’s take a look at the result:

The search behavior is now more appropriate. It returns the expected results and displays the full list of albums when there is no search query.

Let’s make it even better by adding automatic search when the user starts typing in the search field.

Automatic search

To implement automatic search when the user starts typing in the search field, you can use the onChanged method of TextFormField. In this method, we will update the list of albums every time the user enters a new character in the search field:

onChanged: (value) {
_clearAlbums();
_fetchAlbums(value);
},

Now we will be calling the API method on every character input, which is fine from the user’s point of view, but we don’t need the extra load on our end.

There are several ways to optimize search and API requests. We can add a delay before sending the query on user input. This way, if the user enters the queries too quickly, too many requests won’t be sent to the server.

Debouncing

We need to add a delay between text changes in the search field. We can use existing packages (🤡) for this, but we will write our own (🤌)Debouncer class:

import 'dart:async';

class Debouncer {
final int milliseconds;
VoidCallback? action;
Timer? _timer;

Debouncer({required this.milliseconds});

void run(VoidCallback action) {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}

Next, we’ll add a new field to the _AlbumListWidgetState class to create an instance of the Debouncer:

final Debouncer _debouncer = Debouncer(milliseconds: 500);

Now, let’s modify the _fetchAlbums() method again as follows:

_fetchAlbums([String? query]) async {
setState(() {
_isLoading = true;
});

List<Album> newAlbums;
if (query?.isNotEmpty == true) {
_clearAlbums();
newAlbums = await _apiProvider.searchAlbums(query!);
} else {
newAlbums = await _apiProvider.fetchAlbums(_currentPage, _pageSize);
}

await _fetchArtists(newAlbums);

setState(() {
_isLoading = false;
_albums.addAll(newAlbums);
_currentPage++;
});
}

Let’s replace the onChanged handler with the following:

onChanged: (value) {
_debouncer.run(() {
_clearAlbums();
_fetchAlbums(value);
});
},

Now, every time the text changes in the search field, a 500-millisecond timer is started. The timer will reset if the user enters text faster than every 500 milliseconds. If the user stops typing for 500 milliseconds or more, the timer will fire and execute the search with the entered text. This way, we can ensure that the search query is executed only after a delay of 500 milliseconds, which avoids unnecessary API calls and makes the search more efficient.

Oh, look ma, it’s so smooth now:

Summary

In this article, we have explored how to implement full-text search for a Flutter application using a REST API. We used the debouncing method to optimize search in addition to previously applied Cached Network Image library for image caching, and previously implemented capability of binding data from different sources using async functions.

In future articles, we can cover topics such as:

  • Implementing authentication and authorization in a Flutter application using a REST API
  • Using Firebase for data storage and real-time implementation
  • Working with databases in a Flutter application, including local storage and remote databases
  • Using the BLoC pattern for managing the application state and handling user input
  • Implementing push notifications in a Flutter application using Firebase Cloud Messaging.

Thank you for reading, and I hope this article has helped you understand how to implement full-text search in a Flutter application.

Subscribe and don’t miss new materials!

--

--

Alex Josef Bigler
Full Struggle Developer

Enthusiast of new technologies, entrepreneur, and researcher. Writing about IT, economics, and other stuff. Exploring the world through the lens of data.