diff --git a/examples/api/lib/material/autocomplete/autocomplete.2.dart b/examples/api/lib/material/autocomplete/autocomplete.2.dart new file mode 100644 index 0000000000..62d9b388d1 --- /dev/null +++ b/examples/api/lib/material/autocomplete/autocomplete.2.dart @@ -0,0 +1,89 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [Autocomplete] that shows how to fetch the options +/// from a remote API. + +const Duration fakeAPIDuration = Duration(seconds: 1); + +void main() => runApp(const AutocompleteExampleApp()); + +class AutocompleteExampleApp extends StatelessWidget { + const AutocompleteExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Autocomplete - async'), + ), + body: const Center( + child: _AsyncAutocomplete(), + ), + ), + ); + } +} + +class _AsyncAutocomplete extends StatefulWidget { + const _AsyncAutocomplete(); + + @override + State<_AsyncAutocomplete > createState() => _AsyncAutocompleteState(); +} + +class _AsyncAutocompleteState extends State<_AsyncAutocomplete > { + // The query currently being searched for. If null, there is no pending + // request. + String? _searchingWithQuery; + + // The most recent options received from the API. + late Iterable _lastOptions = []; + + @override + Widget build(BuildContext context) { + return Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) async { + _searchingWithQuery = textEditingValue.text; + final Iterable options = await _FakeAPI.search(_searchingWithQuery!); + + // If another search happened after this one, throw away these options. + // Use the previous options intead and wait for the newer request to + // finish. + if (_searchingWithQuery != textEditingValue.text) { + return _lastOptions; + } + + _lastOptions = options; + return options; + }, + onSelected: (String selection) { + debugPrint('You just selected $selection'); + }, + ); + } +} + +// Mimics a remote API. +class _FakeAPI { + static const List _kOptions = [ + 'aardvark', + 'bobcat', + 'chameleon', + ]; + + // Searches the options, but injects a fake "network" delay. + static Future> search(String query) async { + await Future.delayed(fakeAPIDuration); // Fake 1 second delay. + if (query == '') { + return const Iterable.empty(); + } + return _kOptions.where((String option) { + return option.contains(query.toLowerCase()); + }); + } +} diff --git a/examples/api/lib/material/autocomplete/autocomplete.3.dart b/examples/api/lib/material/autocomplete/autocomplete.3.dart new file mode 100644 index 0000000000..ff8e24f560 --- /dev/null +++ b/examples/api/lib/material/autocomplete/autocomplete.3.dart @@ -0,0 +1,167 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [Autocomplete] that demonstrates fetching the +/// options asynchronously and debouncing the network calls. + +const Duration fakeAPIDuration = Duration(seconds: 1); +const Duration debounceDuration = Duration(milliseconds: 500); + +void main() => runApp(const AutocompleteExampleApp()); + +class AutocompleteExampleApp extends StatelessWidget { + const AutocompleteExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Autocomplete - async and debouncing'), + ), + body: const Center( + child: _AsyncAutocomplete(), + ), + ), + ); + } +} + +class _AsyncAutocomplete extends StatefulWidget { + const _AsyncAutocomplete(); + + @override + State<_AsyncAutocomplete > createState() => _AsyncAutocompleteState(); +} + +class _AsyncAutocompleteState extends State<_AsyncAutocomplete > { + // The query currently being searched for. If null, there is no pending + // request. + String? _currentQuery; + + // The most recent options received from the API. + late Iterable _lastOptions = []; + + late final _Debounceable?, String> _debouncedSearch; + + // Calls the "remote" API to search with the given query. Returns null when + // the call has been made obsolete. + Future?> _search(String query) async { + _currentQuery = query; + + // In a real application, there should be some error handling here. + final Iterable options = await _FakeAPI.search(_currentQuery!); + + // If another search happened after this one, throw away these options. + if (_currentQuery != query) { + _currentQuery = null; + return null; + } + _currentQuery = null; + + return options; + } + + @override + void initState() { + super.initState(); + _debouncedSearch = _debounce?, String>(_search); + } + + @override + Widget build(BuildContext context) { + return Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) async { + final Iterable? options = await _debouncedSearch(textEditingValue.text); + if (options == null) { + return _lastOptions; + } + _lastOptions = options; + return options; + }, + onSelected: (String selection) { + debugPrint('You just selected $selection'); + }, + ); + } +} + +// Mimics a remote API. +class _FakeAPI { + static const List _kOptions = [ + 'aardvark', + 'bobcat', + 'chameleon', + ]; + + // Searches the options, but injects a fake "network" delay. + static Future> search(String query) async { + await Future.delayed(fakeAPIDuration); // Fake 1 second delay. + if (query == '') { + return const Iterable.empty(); + } + return _kOptions.where((String option) { + return option.contains(query.toLowerCase()); + }); + } +} + +typedef _Debounceable = Future Function(T parameter); + +/// Returns a new function that is a debounced version of the given function. +/// +/// This means that the original function will be called only after no calls +/// have been made for the given Duration. +_Debounceable _debounce(_Debounceable function) { + _DebounceTimer? debounceTimer; + + return (T parameter) async { + if (debounceTimer != null && !debounceTimer!.isCompleted) { + debounceTimer!.cancel(); + } + debounceTimer = _DebounceTimer(); + try { + await debounceTimer!.future; + } catch (error) { + if (error is _CancelException) { + return null; + } + rethrow; + } + return function(parameter); + }; +} + +// A wrapper around Timer used for debouncing. +class _DebounceTimer { + _DebounceTimer( + ) { + _timer = Timer(debounceDuration, _onComplete); + } + + late final Timer _timer; + final Completer _completer = Completer(); + + void _onComplete() { + _completer.complete(); + } + + Future get future => _completer.future; + + bool get isCompleted => _completer.isCompleted; + + void cancel() { + _timer.cancel(); + _completer.completeError(const _CancelException()); + } +} + +// An exception indicating that the timer was canceled. +class _CancelException implements Exception { + const _CancelException(); +} diff --git a/examples/api/lib/material/autocomplete/autocomplete.4.dart b/examples/api/lib/material/autocomplete/autocomplete.4.dart new file mode 100644 index 0000000000..1c00ad22ab --- /dev/null +++ b/examples/api/lib/material/autocomplete/autocomplete.4.dart @@ -0,0 +1,227 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [Autocomplete] that demonstrates fetching the +/// options asynchronously and debouncing the network calls, including handling +/// network errors. + +void main() => runApp(const AutocompleteExampleApp()); + +const Duration fakeAPIDuration = Duration(seconds: 1); +const Duration debounceDuration = Duration(milliseconds: 500); + +class AutocompleteExampleApp extends StatelessWidget { + const AutocompleteExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Autocomplete - async, debouncing, and network errors'), + ), + body: const Center( + child: _AsyncAutocomplete(), + ), + ), + ); + } +} + +class _AsyncAutocomplete extends StatefulWidget { + const _AsyncAutocomplete(); + + @override + State<_AsyncAutocomplete > createState() => _AsyncAutocompleteState(); +} + +class _AsyncAutocompleteState extends State<_AsyncAutocomplete > { + // The query currently being searched for. If null, there is no pending + // request. + String? _currentQuery; + + // The most recent options received from the API. + late Iterable _lastOptions = []; + + late final _Debounceable?, String> _debouncedSearch; + + // Whether to consider the fake network to be offline. + bool _networkEnabled = true; + + // A network error was recieved on the most recent query. + bool _networkError = false; + + // Calls the "remote" API to search with the given query. Returns null when + // the call has been made obsolete. + Future?> _search(String query) async { + _currentQuery = query; + + late final Iterable options; + try { + options = await _FakeAPI.search(_currentQuery!, _networkEnabled); + } catch (error) { + if (error is _NetworkException) { + setState(() { + _networkError = true; + }); + return []; + } + rethrow; + } + + // If another search happened after this one, throw away these options. + if (_currentQuery != query) { + return null; + } + _currentQuery = null; + + return options; + } + + @override + void initState() { + super.initState(); + _debouncedSearch = _debounce?, String>(_search); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _networkEnabled + ? 'Network is on, toggle to induce network errors.' + : 'Network is off, toggle to allow requests to go through.', + ), + Switch( + value: _networkEnabled, + onChanged: (bool? value) { + setState(() { + _networkEnabled = !_networkEnabled; + }); + }, + ), + const SizedBox( + height: 32.0, + ), + Autocomplete( + fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) { + return TextFormField( + decoration: InputDecoration( + errorText: _networkError ? 'Network error, please try again.' : null, + ), + controller: controller, + focusNode: focusNode, + onFieldSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + optionsBuilder: (TextEditingValue textEditingValue) async { + setState(() { + _networkError = false; + }); + final Iterable? options = await _debouncedSearch(textEditingValue.text); + if (options == null) { + return _lastOptions; + } + _lastOptions = options; + return options; + }, + onSelected: (String selection) { + debugPrint('You just selected $selection'); + }, + ), + ], + ); + } +} + +// Mimics a remote API. +class _FakeAPI { + static const List _kOptions = [ + 'aardvark', + 'bobcat', + 'chameleon', + ]; + + // Searches the options, but injects a fake "network" delay. + static Future> search(String query, bool networkEnabled) async { + await Future.delayed(fakeAPIDuration); // Fake 1 second delay. + if (!networkEnabled) { + throw const _NetworkException(); + } + if (query == '') { + return const Iterable.empty(); + } + return _kOptions.where((String option) { + return option.contains(query.toLowerCase()); + }); + } +} + +typedef _Debounceable = Future Function(T parameter); + +/// Returns a new function that is a debounced version of the given function. +/// +/// This means that the original function will be called only after no calls +/// have been made for the given Duration. +_Debounceable _debounce(_Debounceable function) { + _DebounceTimer? debounceTimer; + + return (T parameter) async { + if (debounceTimer != null && !debounceTimer!.isCompleted) { + debounceTimer!.cancel(); + } + debounceTimer = _DebounceTimer(); + try { + await debounceTimer!.future; + } catch (error) { + if (error is _CancelException) { + return null; + } + rethrow; + } + return function(parameter); + }; +} + +// A wrapper around Timer used for debouncing. +class _DebounceTimer { + _DebounceTimer( + ) { + _timer = Timer(debounceDuration, _onComplete); + } + + late final Timer _timer; + final Completer _completer = Completer(); + + void _onComplete() { + _completer.complete(); + } + + Future get future => _completer.future; + + bool get isCompleted => _completer.isCompleted; + + void cancel() { + _timer.cancel(); + _completer.completeError(const _CancelException()); + } +} + +// An exception indicating that the timer was canceled. +class _CancelException implements Exception { + const _CancelException(); +} + +// An exception indicating that a network request has failed. +class _NetworkException implements Exception { + const _NetworkException(); +} diff --git a/examples/api/test/material/autocomplete/autocomplete.0_test.dart b/examples/api/test/material/autocomplete/autocomplete.0_test.dart new file mode 100644 index 0000000000..70d12aecc7 --- /dev/null +++ b/examples/api/test/material/autocomplete/autocomplete.0_test.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/autocomplete/autocomplete.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('can search and find options', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'a'); + await tester.pump(); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsOneWidget); + expect(find.text('chameleon'), findsOneWidget); + + await tester.enterText(find.byType(TextFormField), 'aa'); + await tester.pump(); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + }); +} diff --git a/examples/api/test/material/autocomplete/autocomplete.1_test.dart b/examples/api/test/material/autocomplete/autocomplete.1_test.dart new file mode 100644 index 0000000000..9dc0510ec6 --- /dev/null +++ b/examples/api/test/material/autocomplete/autocomplete.1_test.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/autocomplete/autocomplete.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('can search and find options by email and name', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + expect(find.text('Alice'), findsNothing); + expect(find.text('Bob'), findsNothing); + expect(find.text('Charlie'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'Ali'); + await tester.pump(); + + expect(find.text('Alice'), findsOneWidget); + expect(find.text('Bob'), findsNothing); + expect(find.text('Charlie'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'gmail'); + await tester.pump(); + + expect(find.text('Alice'), findsNothing); + expect(find.text('Bob'), findsNothing); + expect(find.text('Charlie'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/autocomplete/autocomplete.2_test.dart b/examples/api/test/material/autocomplete/autocomplete.2_test.dart new file mode 100644 index 0000000000..0ee047af7d --- /dev/null +++ b/examples/api/test/material/autocomplete/autocomplete.2_test.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/autocomplete/autocomplete.2.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('can search and find options after waiting for fake network delay', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'a'); + await tester.pump(example.fakeAPIDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsOneWidget); + expect(find.text('chameleon'), findsOneWidget); + + await tester.enterText(find.byType(TextFormField), 'aa'); + await tester.pump(example.fakeAPIDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + }); +} diff --git a/examples/api/test/material/autocomplete/autocomplete.3_test.dart b/examples/api/test/material/autocomplete/autocomplete.3_test.dart new file mode 100644 index 0000000000..a0965a4908 --- /dev/null +++ b/examples/api/test/material/autocomplete/autocomplete.3_test.dart @@ -0,0 +1,80 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/autocomplete/autocomplete.3.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('can search and find options after waiting for fake network delay and debounce delay', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'a'); + await tester.pump(example.fakeAPIDuration); + + // No results yet, need to also wait for the debounce duration. + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.pump(example.debounceDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsOneWidget); + expect(find.text('chameleon'), findsOneWidget); + + await tester.enterText(find.byType(TextFormField), 'aa'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + }); + + testWidgets('debounce is reset each time a character is entered', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + await tester.enterText(find.byType(TextFormField), 'c'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'ch'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'cha'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'cham'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + // Despite the total elapsed time being greater than debounceDuration + + // fakeAPIDuration, the search has not yet completed, because the debounce + // was reset each time text input happened. + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'chame'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/autocomplete/autocomplete.4_test.dart b/examples/api/test/material/autocomplete/autocomplete.4_test.dart new file mode 100644 index 0000000000..12b1154fd6 --- /dev/null +++ b/examples/api/test/material/autocomplete/autocomplete.4_test.dart @@ -0,0 +1,119 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/autocomplete/autocomplete.4.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('can search and find options after waiting for fake network delay and debounce delay', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'a'); + await tester.pump(example.fakeAPIDuration); + + // No results yet, need to also wait for the debounce duration. + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.pump(example.debounceDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsOneWidget); + expect(find.text('chameleon'), findsOneWidget); + + await tester.enterText(find.byType(TextFormField), 'aa'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsOneWidget); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + }); + + testWidgets('debounce is reset each time a character is entered', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + await tester.enterText(find.byType(TextFormField), 'c'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'ch'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'cha'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'cham'); + await tester.pump(example.debounceDuration - const Duration(milliseconds: 100)); + + // Despite the total elapsed time being greater than debounceDuration + + // fakeAPIDuration, the search has not yet completed, because the debounce + // was reset each time text input happened. + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'chame'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsOneWidget); + }); + + testWidgets('shows an error message for network errors', (WidgetTester tester) async { + await tester.pumpWidget(const example.AutocompleteExampleApp()); + + await tester.enterText(find.byType(TextFormField), 'chame'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsOneWidget); + InputDecorator inputDecorator = tester.widget(find.byType(InputDecorator)); + expect(inputDecorator.decoration.errorText, isNull); + + // Turn the network off. + await tester.tap(find.byType(Switch)); + await tester.pump(); + + await tester.enterText(find.byType(TextFormField), 'chamel'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsNothing); + inputDecorator = tester.widget(find.byType(InputDecorator)); + expect(inputDecorator.decoration.errorText, isNotNull); + + // Turn the network back on. + await tester.tap(find.byType(Switch)); + await tester.pump(); + + await tester.enterText(find.byType(TextFormField), 'chamele'); + await tester.pump(example.debounceDuration + example.fakeAPIDuration); + + expect(find.text('aardvark'), findsNothing); + expect(find.text('bobcat'), findsNothing); + expect(find.text('chameleon'), findsOneWidget); + inputDecorator = tester.widget(find.byType(InputDecorator)); + expect(inputDecorator.decoration.errorText, isNull); + }); +} diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index f3e528b619..9f00633c9c 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -28,6 +28,30 @@ import 'theme.dart'; /// ** See code in examples/api/lib/material/autocomplete/autocomplete.1.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows how to create an Autocomplete widget whose options are +/// fetched over the network. +/// +/// ** See code in examples/api/lib/material/autocomplete/autocomplete.2.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows how to create an Autocomplete widget whose options are +/// fetched over the network. It uses debouncing to wait to perform the network +/// request until after the user finishes typing. +/// +/// ** See code in examples/api/lib/material/autocomplete/autocomplete.3.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows how to create an Autocomplete widget whose options are +/// fetched over the network. It includes both debouncing and error handling, so +/// that failed network requests show an error to the user and can be recovered +/// from. Try toggling the network Switch widget to simulate going offline. +/// +/// ** See code in examples/api/lib/material/autocomplete/autocomplete.4.dart ** +/// {@end-tool} +/// /// See also: /// /// * [RawAutocomplete], which is what Autocomplete is built upon, and which