TECH

Flutter | Highlight searched text in results

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

Hitesh Verma May 12, 2023 · 4 min. read

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

Read next

Customized Calendar in Flutter

Hey, have you ever wanted a date-picker in your app but not in a dialog box? And you tried all the dart packages out there but cannot change their UIs

Hitesh Verma May 28, 2023 · 9 min read

Flutter: Custom Cupertino Date Picker

Hitesh Verma in TECH
May 28, 2023 · 6 min read

Flutter Theming | The Right Way

Hitesh Verma in TECH
May 12, 2023 · 5 min read