1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/material.dart';
8
9/// Flutter code sample for [SearchAnchor].
10
11const Duration fakeAPIDuration = Duration(seconds: 1);
12const Duration debounceDuration = Duration(milliseconds: 500);
13
14void main() => runApp(const SearchAnchorAsyncExampleApp());
15
16class SearchAnchorAsyncExampleApp extends StatelessWidget {
17 const SearchAnchorAsyncExampleApp({super.key});
18
19 @override
20 Widget build(BuildContext context) {
21 return MaterialApp(
22 home: Scaffold(
23 appBar: AppBar(title: const Text('SearchAnchor - async and debouncing')),
24 body: const Center(child: _AsyncSearchAnchor()),
25 ),
26 );
27 }
28}
29
30class _AsyncSearchAnchor extends StatefulWidget {
31 const _AsyncSearchAnchor();
32
33 @override
34 State<_AsyncSearchAnchor> createState() => _AsyncSearchAnchorState();
35}
36
37class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor> {
38 // The query currently being searched for. If null, there is no pending
39 // request.
40 String? _currentQuery;
41
42 // The most recent suggestions received from the API.
43 late Iterable<Widget> _lastOptions = <Widget>[];
44
45 late final _Debounceable<Iterable<String>?, String> _debouncedSearch;
46
47 // Calls the "remote" API to search with the given query. Returns null when
48 // the call has been made obsolete.
49 Future<Iterable<String>?> _search(String query) async {
50 _currentQuery = query;
51
52 // In a real application, there should be some error handling here.
53 final Iterable<String> options = await _FakeAPI.search(_currentQuery!);
54
55 // If another search happened after this one, throw away these options.
56 if (_currentQuery != query) {
57 return null;
58 }
59 _currentQuery = null;
60
61 return options;
62 }
63
64 @override
65 void initState() {
66 super.initState();
67 _debouncedSearch = _debounce<Iterable<String>?, String>(_search);
68 }
69
70 @override
71 Widget build(BuildContext context) {
72 return SearchAnchor(
73 builder: (BuildContext context, SearchController controller) {
74 return IconButton(
75 icon: const Icon(Icons.search),
76 onPressed: () {
77 controller.openView();
78 },
79 );
80 },
81 suggestionsBuilder: (BuildContext context, SearchController controller) async {
82 final List<String>? options = (await _debouncedSearch(controller.text))?.toList();
83 if (options == null) {
84 return _lastOptions;
85 }
86 _lastOptions = List<ListTile>.generate(options.length, (int index) {
87 final String item = options[index];
88 return ListTile(
89 title: Text(item),
90 onTap: () {
91 debugPrint('You just selected $item');
92 },
93 );
94 });
95
96 return _lastOptions;
97 },
98 );
99 }
100}
101
102// Mimics a remote API.
103class _FakeAPI {
104 static const List<String> _kOptions = <String>['aardvark', 'bobcat', 'chameleon'];
105
106 // Searches the options, but injects a fake "network" delay.
107 static Future<Iterable<String>> search(String query) async {
108 await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay.
109 if (query == '') {
110 return const Iterable<String>.empty();
111 }
112 return _kOptions.where((String option) {
113 return option.contains(query.toLowerCase());
114 });
115 }
116}
117
118typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
119
120/// Returns a new function that is a debounced version of the given function.
121///
122/// This means that the original function will be called only after no calls
123/// have been made for the given Duration.
124_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
125 _DebounceTimer? debounceTimer;
126
127 return (T parameter) async {
128 if (debounceTimer != null && !debounceTimer!.isCompleted) {
129 debounceTimer!.cancel();
130 }
131 debounceTimer = _DebounceTimer();
132 try {
133 await debounceTimer!.future;
134 } on _CancelException {
135 return null;
136 }
137 return function(parameter);
138 };
139}
140
141// A wrapper around Timer used for debouncing.
142class _DebounceTimer {
143 _DebounceTimer() {
144 _timer = Timer(debounceDuration, _onComplete);
145 }
146
147 late final Timer _timer;
148 final Completer<void> _completer = Completer<void>();
149
150 void _onComplete() {
151 _completer.complete();
152 }
153
154 Future<void> get future => _completer.future;
155
156 bool get isCompleted => _completer.isCompleted;
157
158 void cancel() {
159 _timer.cancel();
160 _completer.completeError(const _CancelException());
161 }
162}
163
164// An exception indicating that the timer was canceled.
165class _CancelException implements Exception {
166 const _CancelException();
167}
168

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com