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 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:flutter/material.dart'; |
8 | |
9 | /// Flutter code sample for [SearchAnchor]. |
10 | |
11 | const Duration fakeAPIDuration = Duration(seconds: 1); |
12 | const Duration debounceDuration = Duration(milliseconds: 500); |
13 | |
14 | void main() => runApp(const SearchAnchorAsyncExampleApp()); |
15 | |
16 | class 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 | |
30 | class _AsyncSearchAnchor extends StatefulWidget { |
31 | const _AsyncSearchAnchor(); |
32 | |
33 | @override |
34 | State<_AsyncSearchAnchor> createState() => _AsyncSearchAnchorState(); |
35 | } |
36 | |
37 | class _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. |
103 | class _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 | |
118 | typedef _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. |
142 | class _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. |
165 | class _CancelException implements Exception { |
166 | const _CancelException(); |
167 | } |
168 | |