Add loading and empty state builders for Autocomplete#155506
Add loading and empty state builders for Autocomplete#155506mohsinraza-fdev wants to merge 1 commit intoflutter:masterfrom
Conversation
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group. |
|
@victorsanni can you please review the changes, working on test cases. also i'm not sure if |
|
@mohsinraza-fdev the approach looks nice to me. cc @justinmc @LongCatIsLooong |
|
This looks really exciting. @victorsanni and I have been thinking about how to make Autocomplete fully support loading and empty. I'm swamped this week but I'm writing myself a note to review this next week, sorry for the delay! |
justinmc
left a comment
There was a problem hiding this comment.
Some feedback after playing around with your autocomplete.5 example:
- Focus the field and the loading message appears, but that's probably unnecessary.
- Select an option, then type or backspace. No loading message is shown, but it should be. It does indeed make a request, because after a few seconds you will see the results or the "no results" message.
- I'm having trouble coming up with a reliable repro, but sometimes I saw the "network error" error message and the "no results found" message overlap (see image below). Maybe it's a Flutter layout bug unrelated to this PR, not sure. But maybe we should not display "no results found" when a network error happens? It might make the user think that their query legitimately found no results, when that's not necessarily true.
Sorry for all the nitpicky details on your example. I think it's important for us to be really careful with the examples we publish, because I believe that many users copy them.
Overall I think that loading and empty states are definitely something that we should easily support with Autocomplete, so I'm on board with adding this functionality.
| String? _currentQuery; | ||
|
|
||
| // The most recent options received from the API. | ||
| late Iterable<String> _lastOptions = <String>[]; |
There was a problem hiding this comment.
I think this should be final and not late, since it's already assigned and never reassigned.
| if (_isLoadingOptions && widget.loadingStateBuilder != null) { | ||
| return widget.loadingStateBuilder!(context, _options); | ||
| } | ||
| if (_options.isEmpty && widget.emptyStateBuilder != null) { | ||
| return widget.emptyStateBuilder!(context, _textEditingController.text); | ||
| } | ||
| return widget.optionsViewBuilder(context, _select, _options); |
There was a problem hiding this comment.
What's the advantage of separating out loadingStateBuilder and emptyStateBuilder instead of handling everything inside of optionsViewBuilder? Maybe with the addition of an isLoading parameter or something.
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [RawAutocomplete.optionsViewBuilder], which is of this type. |
There was a problem hiding this comment.
This should be loadingStateBuilder, not optionsViewBuilder. Similarly for emptyStateBuilder below.
Ill working on the example, thanks for highlighting the issues.
We can put these states inside We can definitely put loading and empty state indicators, Which one do you think we should go with? |
|
I'm leaning towards a combined optionsViewBuilder. Looking at how those similar builders are used in Image: flutter/packages/flutter/lib/src/widgets/image.dart Lines 1330 to 1336 in c78c166 They are used at the same time, while in this PR it's always one or the other separately: if (_isLoadingOptions && widget.loadingStateBuilder != null) {
return widget.loadingStateBuilder!(context, _options);
}
if (_options.isEmpty && widget.emptyStateBuilder != null) {
return widget.emptyStateBuilder!(context, _textEditingController.text);
}
return widget.optionsViewBuilder(context, _select, _options);@victorsanni Do you have thoughts on this? If we do modify the parameters of optionsViewBuilder, we might have to deprecate optionsViewBuilder and replace it with a new parameter. |
|
I'm thinking we can use optionsViewBuilder and add an enum to its parameters, like |
| /// can be displayed when [options] is empty. | ||
| /// | ||
| /// The returned widget from this callback will be wrapped in an | ||
| /// [AutocompleteHighlightedOption] inherited widget. This will allow |
There was a problem hiding this comment.
Is this still relevant when there are no options at all?
There was a problem hiding this comment.
(Same question for the loading builder).
| final AutocompleteOptionsViewBuilder<T> optionsViewBuilder; | ||
|
|
||
| /// {@template flutter.widgets.RawAutocomplete.loadingViewBuilder} | ||
| /// Builds a loading view while [optionsBuilder] is being resolved. |
There was a problem hiding this comment.
nit: maybe: the future returned by [options] builder is being resolved.
| /// place in the widget tree as [RawAutocomplete]. To control whether it opens | ||
| /// upward or downward, use [optionsViewOpenDirection]. | ||
| /// | ||
| /// {@endtemplate} |
There was a problem hiding this comment.
Add "Defaults to null" and document what happens if you return null (e.g, does the widget show the previous options view when loading or nothing at all if this is null).
| /// place in the widget tree as [RawAutocomplete]. To control whether it opens | ||
| /// upward or downward, use [optionsViewOpenDirection]. | ||
| /// | ||
| /// {@endtemplate} |
There was a problem hiding this comment.
Add "Defaults to null" and document what happens if you return null.
| _lastFieldText = value.text; | ||
| final int callId = _onChangedCallId; | ||
|
|
||
| final Iterable<T> options = await widget.optionsBuilder(value); |
There was a problem hiding this comment.
probably not related to this PR, but I'm curious why do we still need to run the optionsBuilder if shouldUpdateOptions is already false?
There was a problem hiding this comment.
Since the optionsBuilder uses TextEditingValue as input, we agreed upon letting the optionsBuilder run on non textual changes, but the we don't want to run the update options logic in that case, thats why i had to add this boolean, we wont need this boolean once we deprecate optionsBuilder and use String text instead of TextEditingValue.
you might wanna read the conversation from here #150776 (comment)
There was a problem hiding this comment.
Ah so optionsBuilder is still run in case there are side effects in it. Makes sense to me.
That would be great i think, ill start working on it. |
|
@justinmc @LongCatIsLooong what do you think of an enum approach? Are there better alternatives? |
So the builder callback would return Also, we currently don't show the "outdated" results when |
No we do. Currently on master, the previous options remain visible while the next options are loading. So for example in autocomplete.4.dart, I type "a" and wait for all 3 options to appear, then type a second "a". All 3 options remain on screen until the async operation finishes and then only "aardvark" is displayed. |
|
@victorsanni Could your enum approach be achieved by the user without needing us to pass anything into optionsViewBuilder? Example of the app developer managing request status themselvesreturn Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) async {
_searchingWithQuery = textEditingValue.text;
late final Iterable<String> options;
try {
_requestStatus = _RequestStatus.loading;
options = await _FakeAPI.search(_currentQuery!, _networkEnabled);
_requestStatus = _RequestStatus.inactive;
} on _NetworkException {
_requestStatus = _RequestStatus.error;
if (mounted) {
setState(() {
_networkError = true;
});
}
return <String>[];
}
if (_searchingWithQuery != textEditingValue.text) {
return _lastOptions;
}
_lastOptions = options;
return options;
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<T> onSelected, Iterable<T> options) {
if (options.isEmpty) {
// Don't show the options overlay at all.
return null;
}
return switch (_requestStatus) {
_RequestStatus.inactive => _MyOptions(options: options),
_RequestStatus.loading => const CircularProgressIndicator(),
_RequestStatus.error => const Text('Fail.'),
};
);Then I guess the only code change we would need to make to RawAutocomplete is to call optionsViewBuilder more frequently (possibly a breaking change). Oh and optionsViewBuilder would need to be able to return |
II think it makes sense for users to handle the output of optionsViewBuilder themselves instead of increasing the API surface, as long as we have a clear example on the website. @mohsinraza-fdev can you try this approach to see if it can be done without breaking users? |
Using an inherited widget we can do that i think, just the way we track the highlighted item index.
We can either add the What do you guys think? One more thing, how many state are we talking about, currently i have and do we really need the empty state enum?, currently we hide the options overlay on empty results, but after this implementation we will have to build the overlay on empty results as well so the user can check the empty state from the options variable itself. need a little clarification on this... |
It would be nice to not break users. Would love to see what this implementation would look like. But I really don't know what the pros and cons are. Also, changing the function signature might be better for discoverability.
Currently, users can perform an async operation in optionsBuilder, and if that errors out, they might return some special set of options which they catch in optionsViewBuilder to return the desired view. It would be nice to wrap that logic for them with an error state. But I can't think of other states. Maybe
My understanding is the options can be empty either on first focus or if nothing is found. Maybe those need separate states. I think in general we want to reduce the logic in optionsBuilder so users don't have to add special identifiers to the options returned from optionsBuilder to have different views in optionsViewBuilder. |
|
(PR Triage): Hey @mohsinraza-fdev, would you like to continue working on this change? |
Hey - I've been really swamped with work this month. I'd still love to help with this task, but I might need a bit more time. If it's urgent though, no worries about reassigning it to someone else! |
victorsanni
left a comment
There was a problem hiding this comment.
No rush @mohsinraza-fdev. Feel free to work on it whenever you have some time. For now, we will have to close it or mark it as a draft just to get it off our PR review queue. Thanks for the work so far!
|
This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
(triage): I am going to close this PR since it has gone stale. Please feel free to open a new one when you find the time to work on this again. Thank you. |
This PR introduces two new builders to
AutocompleteoptionsBuildercall is being resolvedoptionsis empty.Fixes #108258
Fixes #147377
Video demonstration:
Screen.Recording.2024-09-22.at.6.32.44.PM.mov
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.