Guides / Building Search UI / Ecommerce ui template / Components
On this page

The Autocomplete mobile component

Code summary

The Autocomplete component is used on the Ecommerce UI demo app search page when you tap the search bar in the main page. It consists of two sections: one presenting the search history and the second one presenting the query suggestions.

The AppBar includes SearchHeaderView` widget representing the search box on the autocomplete screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class SearchHeaderView extends StatelessWidget {
  const SearchHeaderView({Key? key, required this.controller, this.onSubmitted})
      : super(key: key);

  final TextEditingController controller;
  final ValueChanged<String>? onSubmitted;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        IconButton(
            onPressed: () => Navigator.pop(context),
            icon: const Icon(Icons.arrow_back)
        ),
        Expanded(
          child: TextField(
            controller: controller,
            autofocus: true,
            onSubmitted: onSubmitted,
            decoration: const InputDecoration(
              border: InputBorder.none,
              hintText: "Search products, articles, faq, ..."),
          ),
        ),
        if (controller.text.isNotEmpty)
          IconButton(
            iconSize: 34,
                  onPressed: controller.clear,
                  icon: const Icon(Icons.clear),
                  color: AppTheme.darkBlue
              ),
        const SizedBox(width: 0)
      ],
    );
  }
}

Each section consists of couple of SliverAppBar and SliverList components representing the section header and section body respectively.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<Widget> _section<Suggestion>(Widget title, List<Suggestion> items, Function(Suggestion) rowBuilder) {
  return [
    SliverAppBar(
        titleSpacing: 0,
        titleTextStyle: Theme.of(context).textTheme.subtitle2,
        title: title,
        automaticallyImplyLeading: false),
    SliverList(
        delegate: SliverChildBuilderDelegate(
      (BuildContext context, int index) {
        final item = items[index];
        return SizedBox(
            height: 50,
            child: InkWell(
                onTap: () => _submitSearch(item.toString()),
                child: rowBuilder(item)));
      },
      childCount: items.length,
    ))
  ];
}

The first section represents the search history. All the submitted search queries are stored in the autocompelte screen state. User can delete each history item or remove them all by tapping the corresponding button in the section header.

The search history section

Each history row is represented by a HistoryRowView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HistoryRowView extends StatelessWidget {
  const HistoryRowView({Key? key, required this.suggestion, this.onRemove})
      : super(key: key);

  final String suggestion;
  final Function(String)? onRemove;

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      const Icon(Icons.refresh),
      const SizedBox(
        width: 10,
      ),
      Text(suggestion, style: const TextStyle(fontSize: 16)),
      const Spacer(),
      IconButton(
          onPressed: () => onRemove?.call(suggestion),
          icon: const Icon(Icons.close)),
    ]);
  }
}

The second section represents the query suggestions. By default it shows the list of popular searches. When user starts typing a search query the list is refreshing automatically and presents the highlighted search completions.

The popular searches section

Each suggestion row is represented by a SuggestionRowView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SuggestionRowView extends StatelessWidget {
  const SuggestionRowView({Key? key, required this.suggestion, this.onComplete})
      : super(key: key);

  final QuerySuggestion suggestion;
  final Function(String)? onComplete;

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      const Icon(Icons.search),
      const SizedBox(
        width: 10,
      ),
      HighlightedTextView(
          highlighted: suggestion.highlighted!,
          isInverted: true),
      const Spacer(),
      IconButton(
        onPressed: () => onComplete?.call(suggestion.query),
        icon: const Icon(Icons.north_west),
      )
    ]);
  }
}

The highlighting of the suggestion row is possible through HighlightedTextView widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class HighlightedTextView extends StatelessWidget {
  final String highlighted;
  final String preTag;
  final String postTag;
  final bool isInverted;
  final TextStyle regularTextStyle;
  final TextStyle highlightedTextStyle;

  const HighlightedTextView(
      {Key? key,
      required this.highlighted,
      this.preTag = "<em>",
      this.postTag = "</em>",
      this.isInverted = false,
      this.regularTextStyle = const TextStyle(
          fontWeight: FontWeight.normal, color: Colors.black87, fontSize: 15),
      this.highlightedTextStyle = const TextStyle(
          fontWeight: FontWeight.bold, color: Colors.black87, fontSize: 15)})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    List<HighlightedString> strings = [];
    final re = RegExp("$preTag(\\w+)$postTag");
    final matches = re.allMatches(highlighted).toList();

    void append(String string, bool isHighlighted) {
      strings.add(HighlightedString(string, isHighlighted));
    }

    int prev = 0;
    for (final match in matches) {
      if (prev != match.start) {
        append(highlighted.substring(prev, match.start), isInverted);
      }
      append(match.group(1)!, !isInverted);
      prev = match.end;
    }
    if (prev != highlighted.length) {
      append(highlighted.substring(prev), isInverted);
    }

    final spans = strings
        .map((string) => TextSpan(
            text: string.string,
            style:
                string.isHighlighted ? highlightedTextStyle : regularTextStyle))
        .toList();

    return RichText(
      text: TextSpan(
        style: const TextStyle(color: Colors.black87, fontSize: 15),
        children: spans,
      ),
    );
  }
}

The entire autocomplete logic can be found in the autocomplete_screen file.

You can customize Autocomplete in:

Did you find this page helpful?