You might have noticed when the user writes something in the search field, the searched text is highlighted in each result in the list below. It gives an understanding of why a data is included in the result.

Highlighted searched text in flutter list
Let's see how we can achieve this in Flutter. We will be showing a list of countries along with their codes. For this we have stored the json file country_codes.json containing countries & their codes in the assets/res folder of our project, which you can download from here.
Loading sample data on initState()
class _SearchHighlightListViewState extends State {
Timer? _timer;
String _searchText = '';
List _data = [];
List _searchResults = [];
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_loadData();
}
Future _loadData() async {
final str = await rootBundle.loadString('assets/res/country_codes.json');
final data = jsonDecode(str);
_data = (data as List).map((e) => CountryModel(e['code'], e['name'])).toList();
setState(() {});
}
}
class CountryModel {
final String code;
final String name;
const CountryModel(this.code, this.name);
}Add a search text field & a list
Add a text field for searching countries & a list which will either show all the countries or the specific search results.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
_searchField,
Expanded(child: _countryListView),
],
),
);
}
Widget get _searchField {
return Padding(
padding: const EdgeInsets.all(20.0),
child: TextFormField(
onChanged: _onChanged,
controller: _controller,
decoration: const InputDecoration(
hintText: 'Search Country',
suffixIcon: Icon(Icons.search),
),
),
);
}
Widget get _countryListView {
// show search results list
if (_searchResults.isNotEmpty) {
return ListView.separated(
padding: const EdgeInsets.all(20),
itemCount: _searchResults.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) => Row(
children: [
SizedBox(width: 100, child: Text(_searchResults[index].code)),
Expanded(child: _formattedName(_searchResults[index].name)),
],
),
);
}
// show all countries list
return ListView.separated(
itemCount: _data.length,
padding: const EdgeInsets.all(20),
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) => Row(
children: [
SizedBox(width: 100, child: Text(_data[index].code)),
Expanded(child: Text(_data[index].name)),
],
),
);
}
/// format country name text with highlighted search text
Widget _formattedName(String name) {
int index = name.toLowerCase().indexOf(_searchText.toLowerCase());
if (index == -1) return Text(name);
// text before search text in the country name
String prefix = name.substring(0, index);
// actual search text
String boldText = name.substring(index, index + _searchText.length);
// text after search text in the country name
String suffix = name.substring(index + _searchText.length);
return RichText(
text: TextSpan(
text: prefix,
children: [
// make search text bold
TextSpan(
text: boldText,
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: suffix),
],
),
);
}Search when the text field value changes
void _onChanged(String text) {
// cancel previous searching process, when new text is inputted
_timer?.cancel();
_timer = null;
// cancel & clear search when textfield is empty, to show all countries
if (_controller.text.trim().isEmpty) {
_searchText = '';
_searchResults.clear();
setState(() {});
} else {
// start timer which we trigger search API after 1 second of user stopped typing
_timer = Timer(const Duration(seconds: 1), _search);
}
}
void _search() {
_searchText = _controller.text.toLowerCase();
_searchResults = _data.where((e) => e.name.toLowerCase().contains(_searchText)).toList();
setState(() {});
}Complete Code
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MaterialApp(home: SearchHighlightListView()));
class SearchHighlightListView extends StatefulWidget {
const SearchHighlightListView({super.key});
@override
State createState() => _SearchHighlightListViewState();
}
class _SearchHighlightListViewState extends State {
Timer? _timer;
String _searchText = '';
List _data = [];
List _searchResults = [];
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_loadData();
}
@override
void dispose() {
_timer?.cancel();
_timer = null;
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
_searchField,
Expanded(child: _countryListView),
],
),
);
}
Widget get _searchField {
return Padding(
padding: const EdgeInsets.all(20.0),
child: TextFormField(
onChanged: _onChanged,
controller: _controller,
decoration: const InputDecoration(
hintText: 'Search Country',
suffixIcon: Icon(Icons.search),
),
),
);
}
Widget get _countryListView {
// show search results list
if (_searchResults.isNotEmpty) {
return ListView.separated(
padding: const EdgeInsets.all(20),
itemCount: _searchResults.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) => Row(
children: [
SizedBox(width: 100, child: Text(_searchResults[index].code)),
Expanded(child: _formattedName(_searchResults[index].name)),
],
),
);
}
// show all countries list
return ListView.separated(
itemCount: _data.length,
padding: const EdgeInsets.all(20),
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) => Row(
children: [
SizedBox(width: 100, child: Text(_data[index].code)),
Expanded(child: Text(_data[index].name)),
],
),
);
}
/// format country name text with highlighted search text
Widget _formattedName(String name) {
int index = name.toLowerCase().indexOf(_searchText.toLowerCase());
if (index == -1) return Text(name);
// text before search text in the country name
String prefix = name.substring(0, index);
// actual search text
String boldText = name.substring(index, index + _searchText.length);
// text after search text in the country name
String suffix = name.substring(index + _searchText.length);
return RichText(
text: TextSpan(
text: prefix,
children: [
// make search text bold
TextSpan(
text: boldText,
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: suffix),
],
),
);
}
Future _loadData() async {
final str = await rootBundle.loadString('assets/res/country_codes.json');
final data = jsonDecode(str);
_data = (data as List).map((e) => CountryModel(e['code'], e['name'])).toList();
setState(() {});
}
void _onChanged(String text) {
// cancel previous searching process, when new text is inputted
_timer?.cancel();
_timer = null;
// cancel & clear search when textfield is empty, to show all countries
if (_controller.text.trim().isEmpty) {
_searchText = '';
_searchResults.clear();
setState(() {});
} else {
// start timer which we trigger search API after 1 second of user stopped typing
_timer = Timer(const Duration(seconds: 1), _search);
}
}
void _search() {
_searchText = _controller.text.toLowerCase();
_searchResults = _data.where((e) => e.name.toLowerCase().contains(_searchText)).toList();
setState(() {});
}
}
class CountryModel {
final String code;
final String name;
const CountryModel(this.code, this.name);
}Thank you for reading this article. I hope it helped you :-)