Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
7b67aa58
Unverified
Commit
7b67aa58
authored
May 24, 2023
by
Sun Jiao
Committed by
GitHub
May 24, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
make `suggestionsBuilder` in `SearchAnchor` asyncable (#127019)
parent
a19b3436
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
454 additions
and
7 deletions
+454
-7
search_anchor.3.dart
examples/api/lib/material/search_anchor/search_anchor.3.dart
+99
-0
search_anchor.4.dart
examples/api/lib/material/search_anchor/search_anchor.4.dart
+179
-0
search_anchor.3_test.dart
...api/test/material/search_anchor/search_anchor.3_test.dart
+34
-0
search_anchor.4_test.dart
...api/test/material/search_anchor/search_anchor.4_test.dart
+86
-0
search_anchor.dart
packages/flutter/lib/src/material/search_anchor.dart
+11
-7
search_anchor_test.dart
packages/flutter/test/material/search_anchor_test.dart
+45
-0
No files found.
examples/api/lib/material/search_anchor/search_anchor.3.dart
0 → 100644
View file @
7b67aa58
// 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
());
});
}
}
examples/api/lib/material/search_anchor/search_anchor.4.dart
0 → 100644
View file @
7b67aa58
// 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
();
}
examples/api/test/material/search_anchor/search_anchor.3_test.dart
0 → 100644
View file @
7b67aa58
// 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
);
});
}
examples/api/test/material/search_anchor/search_anchor.4_test.dart
0 → 100644
View file @
7b67aa58
// 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.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
.
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
);
// No results yet, need to also wait for the debounce duration.
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
await
tester
.
pump
(
example
.
debounceDuration
);
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
.
debounceDuration
+
example
.
fakeAPIDuration
);
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsOneWidget
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
});
testWidgets
(
'debounce is reset each time a character is entered'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
SearchAnchorAsyncExampleApp
());
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
search
));
await
tester
.
pumpAndSettle
();
await
tester
.
enterText
(
find
.
byType
(
SearchBar
),
'c'
);
await
tester
.
pump
(
example
.
debounceDuration
-
const
Duration
(
milliseconds:
100
));
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
await
tester
.
enterText
(
find
.
byType
(
SearchBar
),
'ch'
);
await
tester
.
pump
(
example
.
debounceDuration
-
const
Duration
(
milliseconds:
100
));
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
await
tester
.
enterText
(
find
.
byType
(
SearchBar
),
'cha'
);
await
tester
.
pump
(
example
.
debounceDuration
-
const
Duration
(
milliseconds:
100
));
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
await
tester
.
enterText
(
find
.
byType
(
SearchBar
),
'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
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsNothing
);
await
tester
.
enterText
(
find
.
byType
(
SearchBar
),
'chame'
);
await
tester
.
pump
(
example
.
debounceDuration
+
example
.
fakeAPIDuration
);
expect
(
find
.
widgetWithText
(
ListTile
,
'aardvark'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'bobcat'
),
findsNothing
);
expect
(
find
.
widgetWithText
(
ListTile
,
'chameleon'
),
findsOneWidget
);
});
}
packages/flutter/lib/src/material/search_anchor.dart
View file @
7b67aa58
...
...
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:async'
;
import
'dart:math'
as
math
;
import
'dart:ui'
;
...
...
@@ -46,7 +47,7 @@ typedef SearchAnchorChildBuilder = Widget Function(BuildContext context, SearchC
///
/// The `controller` callback provided to [SearchAnchor.suggestionsBuilder] can be used
/// to close the search view and control the editable field on the view.
typedef
SuggestionsBuilder
=
Iterable
<
Widget
>
Function
(
BuildContext
context
,
SearchController
controller
);
typedef
SuggestionsBuilder
=
FutureOr
<
Iterable
<
Widget
>
>
Function
(
BuildContext
context
,
SearchController
controller
);
/// Signature for a function that creates a [Widget] to layout the suggestion list.
///
...
...
@@ -648,7 +649,7 @@ class _ViewContentState extends State<_ViewContent> {
Size
?
_screenSize
;
late
Rect
_viewRect
;
late
final
SearchController
_controller
;
late
Iterable
<
Widget
>
result
;
Iterable
<
Widget
>
result
=
<
Widget
>[]
;
final
FocusNode
_focusNode
=
FocusNode
();
@override
...
...
@@ -674,7 +675,6 @@ class _ViewContentState extends State<_ViewContent> {
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
result
=
widget
.
suggestionsBuilder
(
context
,
_controller
);
final
Size
updatedScreenSize
=
MediaQuery
.
of
(
context
).
size
;
if
(
_screenSize
!=
updatedScreenSize
)
{
...
...
@@ -683,6 +683,7 @@ class _ViewContentState extends State<_ViewContent> {
_viewRect
=
Offset
.
zero
&
_screenSize
!;
}
}
unawaited
(
updateSuggestions
());
}
Widget
viewBuilder
(
Iterable
<
Widget
>
suggestions
)
{
...
...
@@ -698,10 +699,13 @@ class _ViewContentState extends State<_ViewContent> {
return
widget
.
viewBuilder
!(
suggestions
);
}
void
updateSuggestions
()
{
setState
(()
{
result
=
widget
.
suggestionsBuilder
(
context
,
_controller
);
});
Future
<
void
>
updateSuggestions
()
async
{
final
Iterable
<
Widget
>
suggestions
=
await
widget
.
suggestionsBuilder
(
context
,
_controller
);
if
(
mounted
)
{
setState
(()
{
result
=
suggestions
;
});
}
}
@override
...
...
packages/flutter/test/material/search_anchor_test.dart
View file @
7b67aa58
...
...
@@ -1362,6 +1362,51 @@ void main() {
expect
(
controller
.
value
.
text
,
suggestion
);
});
testWidgets
(
'SearchAnchor suggestionsBuilder property could be async'
,
(
WidgetTester
tester
)
async
{
final
SearchController
controller
=
SearchController
();
const
String
suggestion
=
'suggestion text'
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
Material
(
child:
Align
(
alignment:
Alignment
.
topCenter
,
child:
SearchAnchor
(
searchController:
controller
,
builder:
(
BuildContext
context
,
SearchController
controller
)
{
return
const
Icon
(
Icons
.
search
);
},
suggestionsBuilder:
(
BuildContext
context
,
SearchController
controller
)
async
{
return
<
Widget
>[
ListTile
(
title:
const
Text
(
suggestion
),
onTap:
()
{
setState
(()
{
controller
.
closeView
(
suggestion
);
});
},
),
];
},
),
),
);
},
),
));
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
search
));
await
tester
.
pumpAndSettle
();
final
Finder
text
=
find
.
text
(
suggestion
);
expect
(
text
,
findsOneWidget
);
await
tester
.
tap
(
text
);
await
tester
.
pumpAndSettle
();
expect
(
controller
.
isOpen
,
false
);
expect
(
controller
.
value
.
text
,
suggestion
);
});
testWidgets
(
'SearchAnchor.bar has a default search bar as the anchor'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment