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