forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
454 additions
and
7 deletions.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
examples/api/lib/material/search_anchor/search_anchor.3.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// 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 [SearchAnchor] that shows how to fetch the suggestions | ||
/// from a remote API. | ||
const Duration fakeAPIDuration = Duration(seconds: 1); | ||
|
||
void main() => runApp(const SearchAnchorAsyncExampleApp()); | ||
|
||
class SearchAnchorAsyncExampleApp extends StatelessWidget { | ||
const SearchAnchorAsyncExampleApp({super.key}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar( | ||
title: const Text('SearchAnchor - async'), | ||
), | ||
body: const Center( | ||
child: _AsyncSearchAnchor(), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
class _AsyncSearchAnchor extends StatefulWidget { | ||
const _AsyncSearchAnchor(); | ||
|
||
@override | ||
State<_AsyncSearchAnchor > createState() => _AsyncSearchAnchorState(); | ||
} | ||
|
||
class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor > { | ||
// 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<Widget> _lastOptions = <Widget>[]; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return SearchAnchor( | ||
builder: (BuildContext context, SearchController controller) { | ||
return IconButton( | ||
icon: const Icon(Icons.search), | ||
onPressed: () { | ||
controller.openView(); | ||
}, | ||
); | ||
}, | ||
suggestionsBuilder: (BuildContext context, SearchController controller) async { | ||
_searchingWithQuery = controller.text; | ||
final List<String> options = (await _FakeAPI.search(_searchingWithQuery!)).toList(); | ||
|
||
// 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 != controller.text) { | ||
return _lastOptions; | ||
} | ||
|
||
_lastOptions = List<ListTile>.generate(options.length, (int index) { | ||
final String item = options[index]; | ||
return ListTile( | ||
title: Text(item), | ||
); | ||
}); | ||
|
||
return _lastOptions; | ||
}); | ||
} | ||
} | ||
|
||
// Mimics a remote API. | ||
class _FakeAPI { | ||
static const List<String> _kOptions = <String>[ | ||
'aardvark', | ||
'bobcat', | ||
'chameleon', | ||
]; | ||
|
||
// Searches the options, but injects a fake "network" delay. | ||
static Future<Iterable<String>> search(String query) async { | ||
await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay. | ||
if (query == '') { | ||
return const Iterable<String>.empty(); | ||
} | ||
return _kOptions.where((String option) { | ||
return option.contains(query.toLowerCase()); | ||
}); | ||
} | ||
} |
179 changes: 179 additions & 0 deletions
179
examples/api/lib/material/search_anchor/search_anchor.4.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// 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 [SearchAnchor] that demonstrates fetching the | ||
/// suggestions asynchronously and debouncing the network calls. | ||
const Duration fakeAPIDuration = Duration(seconds: 1); | ||
const Duration debounceDuration = Duration(milliseconds: 500); | ||
|
||
void main() => runApp(const SearchAnchorAsyncExampleApp()); | ||
|
||
class SearchAnchorAsyncExampleApp extends StatelessWidget { | ||
const SearchAnchorAsyncExampleApp({super.key}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar( | ||
title: const Text('SearchAnchor - async and debouncing'), | ||
), | ||
body: const Center( | ||
child: _AsyncSearchAnchor(), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
class _AsyncSearchAnchor extends StatefulWidget { | ||
const _AsyncSearchAnchor(); | ||
|
||
@override | ||
State<_AsyncSearchAnchor > createState() => _AsyncSearchAnchorState(); | ||
} | ||
|
||
class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor > { | ||
// The query currently being searched for. If null, there is no pending | ||
// request. | ||
String? _currentQuery; | ||
|
||
// The most recent suggestions received from the API. | ||
late Iterable<Widget> _lastOptions = <Widget>[]; | ||
|
||
late final _Debounceable<Iterable<String>?, String> _debouncedSearch; | ||
|
||
// Calls the "remote" API to search with the given query. Returns null when | ||
// the call has been made obsolete. | ||
Future<Iterable<String>?> _search(String query) async { | ||
_currentQuery = query; | ||
|
||
// In a real application, there should be some error handling here. | ||
final Iterable<String> options = await _FakeAPI.search(_currentQuery!); | ||
|
||
// 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<Iterable<String>?, String>(_search); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return SearchAnchor( | ||
builder: (BuildContext context, SearchController controller) { | ||
return IconButton( | ||
icon: const Icon(Icons.search), | ||
onPressed: () { | ||
controller.openView(); | ||
}, | ||
); | ||
}, | ||
suggestionsBuilder: (BuildContext context, SearchController controller) async { | ||
final List<String>? options = (await _debouncedSearch(controller.text))?.toList(); | ||
if (options == null) { | ||
return _lastOptions; | ||
} | ||
_lastOptions = List<ListTile>.generate(options.length, (int index) { | ||
final String item = options[index]; | ||
return ListTile( | ||
title: Text(item), | ||
onTap: () { | ||
debugPrint('You just selected $item'); | ||
}, | ||
); | ||
}); | ||
|
||
return _lastOptions; | ||
}, | ||
); | ||
} | ||
} | ||
|
||
// Mimics a remote API. | ||
class _FakeAPI { | ||
static const List<String> _kOptions = <String>[ | ||
'aardvark', | ||
'bobcat', | ||
'chameleon', | ||
]; | ||
|
||
// Searches the options, but injects a fake "network" delay. | ||
static Future<Iterable<String>> search(String query) async { | ||
await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay. | ||
if (query == '') { | ||
return const Iterable<String>.empty(); | ||
} | ||
return _kOptions.where((String option) { | ||
return option.contains(query.toLowerCase()); | ||
}); | ||
} | ||
} | ||
|
||
typedef _Debounceable<S, T> = Future<S?> 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<S, T> _debounce<S, T>(_Debounceable<S?, T> 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<void> _completer = Completer<void>(); | ||
|
||
void _onComplete() { | ||
_completer.complete(); | ||
} | ||
|
||
Future<void> 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(); | ||
} |
34 changes: 34 additions & 0 deletions
34
examples/api/test/material/search_anchor/search_anchor.3_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// 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/search_anchor/search_anchor.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', (WidgetTester tester) async { | ||
await tester.pumpWidget(const example.SearchAnchorAsyncExampleApp()); | ||
|
||
await tester.tap(find.byIcon(Icons.search)); | ||
await tester.pumpAndSettle(); | ||
|
||
expect(find.widgetWithText(ListTile, 'aardvark'), findsNothing); | ||
expect(find.widgetWithText(ListTile, 'bobcat'), findsNothing); | ||
expect(find.widgetWithText(ListTile, 'chameleon'), findsNothing); | ||
|
||
await tester.enterText(find.byType(SearchBar), 'a'); | ||
await tester.pump(example.fakeAPIDuration); | ||
|
||
expect(find.widgetWithText(ListTile, 'aardvark'), findsOneWidget); | ||
expect(find.widgetWithText(ListTile, 'bobcat'), findsOneWidget); | ||
expect(find.widgetWithText(ListTile, 'chameleon'), findsOneWidget); | ||
|
||
await tester.enterText(find.byType(SearchBar), 'aa'); | ||
await tester.pump(example.fakeAPIDuration); | ||
|
||
expect(find.widgetWithText(ListTile, 'aardvark'), findsOneWidget); | ||
expect(find.widgetWithText(ListTile, 'bobcat'), findsNothing); | ||
expect(find.widgetWithText(ListTile, 'chameleon'), findsNothing); | ||
}); | ||
} |
Oops, something went wrong.