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 :-)