Skip to content

Add loading and empty state builders for Autocomplete#155506

Closed
mohsinraza-fdev wants to merge 1 commit intoflutter:masterfrom
mohsinraza-fdev:autocomplete-state-builders
Closed

Add loading and empty state builders for Autocomplete#155506
mohsinraza-fdev wants to merge 1 commit intoflutter:masterfrom
mohsinraza-fdev:autocomplete-state-builders

Conversation

@mohsinraza-fdev
Copy link
Contributor

@mohsinraza-fdev mohsinraza-fdev commented Sep 22, 2024

This PR introduces two new builders to Autocomplete

  • A loading view builder which can be used to display custom loading view while optionsBuilder call is being resolved
  • An empty state builder which can be used to display a custom view when options is 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.

@flutter-dashboard
Copy link

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.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Sep 22, 2024
@mohsinraza-fdev
Copy link
Contributor Author

@victorsanni can you please review the changes, working on test cases.

also i'm not sure if loadingViewBuilder would be better or loadingStateBuilder or do you have any other suggestion in mind?

@victorsanni
Copy link
Contributor

@mohsinraza-fdev the approach looks nice to me. cc @justinmc @LongCatIsLooong

@Piinks Piinks requested a review from justinmc October 2, 2024 18:14
@justinmc
Copy link
Contributor

justinmc commented Oct 2, 2024

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!

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Screenshot 2024-10-08 at 9 03 32 AM

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>[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be final and not late, since it's already assigned and never reassigned.

Comment on lines +525 to +531
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be loadingStateBuilder, not optionsViewBuilder. Similarly for emptyStateBuilder below.

@mohsinraza-fdev
Copy link
Contributor Author

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.

Ill working on the example, thanks for highlighting the issues.

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.

We can put these states inside optionsViewBuilder, i looked at other widgets like Image.network which are using separate loading builders so thats why i suggested the idea.

We can definitely put loading and empty state indicators, boolean or enum inside optionsViewBuilder to handle these different views.

Which one do you think we should go with?

@justinmc
Copy link
Contributor

justinmc commented Oct 10, 2024

I'm leaning towards a combined optionsViewBuilder. Looking at how those similar builders are used in Image:

if (widget.frameBuilder != null) {
result = widget.frameBuilder!(context, result, _frameNumber, _wasSynchronouslyLoaded);
}
if (widget.loadingBuilder != null) {
result = widget.loadingBuilder!(context, result, _loadingProgress);
}

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.

@Piinks Piinks requested a review from victorsanni October 23, 2024 18:22
@victorsanni
Copy link
Contributor

I'm thinking we can use optionsViewBuilder and add an enum to its parameters, like optionsStatus or something. And it could be .loading, .empty, .nonEmpty or something like that.

/// can be displayed when [options] is empty.
///
/// The returned widget from this callback will be wrapped in an
/// [AutocompleteHighlightedOption] inherited widget. This will allow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still relevant when there are no options at all?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same question for the loading builder).

final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;

/// {@template flutter.widgets.RawAutocomplete.loadingViewBuilder}
/// Builds a loading view while [optionsBuilder] is being resolved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not related to this PR, but I'm curious why do we still need to run the optionsBuilder if shouldUpdateOptions is already false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so optionsBuilder is still run in case there are side effects in it. Makes sense to me.

@mohsinraza-fdev
Copy link
Contributor Author

I'm thinking we can use optionsViewBuilder and add an enum to its parameters, like optionsStatus or something. And it could be .loading, .empty, .nonEmpty or something like that.

That would be great i think, ill start working on it.

@victorsanni
Copy link
Contributor

@justinmc @LongCatIsLooong what do you think of an enum approach? Are there better alternatives?

@LongCatIsLooong
Copy link
Contributor

LongCatIsLooong commented Oct 24, 2024

@justinmc @LongCatIsLooong what do you think of an enum approach? Are there better alternatives?

So the builder callback would return null to indicate the candidate view should not show, if the developer wants to dismiss the candidate view when empty / loading? Adding additional parameters to the callback sounds reasonable as long as they don't break existing code. But I think it will likely be a breaking change.

Also, we currently don't show the "outdated" results when optionsBuilder future is being resolved right?

@justinmc
Copy link
Contributor

justinmc commented Nov 5, 2024

Also, we currently don't show the "outdated" results when optionsBuilder future is being resolved right?

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.

@justinmc
Copy link
Contributor

justinmc commented Nov 5, 2024

@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 themselves
return 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 null to indicate that the options should not be shown at all. I guess the user can't just return SizedBox.shrink because we need to not show the Overlay at all.

@victorsanni
Copy link
Contributor

Could your enum approach be achieved by the user without needing us to pass anything into optionsViewBuilder?

Then I guess the only code change we would need to make to RawAutocomplete is to call optionsViewBuilder more frequently (possibly a breaking change).

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?

@mohsinraza-fdev
Copy link
Contributor Author

@victorsanni Could your enum approach be achieved by the user without needing us to pass anything into optionsViewBuilder?

Using an inherited widget we can do that i think, just the way we track the highlighted item index.

@mohsinraza-fdev can you try this approach to see if it can be done without breaking users?

We can either add the OptionsViewState enum in optionsViewBuilder as an arguments or create an inherited widget which holds the state.
The builder argument method will change the function signature, but inherited widget method wont break the users.

What do you guys think?

One more thing, how many state are we talking about, currently i have empty, loading and ready in mind, do we need to add and error state or any other states?

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...

@victorsanni
Copy link
Contributor

We can either add the OptionsViewState enum in optionsViewBuilder as an arguments or create an inherited widget which holds the state. The builder argument method will change the function signature, but inherited widget method wont break the users.

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.

One more thing, how many state are we talking about, currently i have empty, loading and ready in mind, do we need to add and error state or any other states?

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 initial, for when the field is focused for the first time? But I don't know if that's already wrapped somewhere else.

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.

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.

@Piinks
Copy link
Contributor

Piinks commented Nov 19, 2024

(PR Triage): Hey @mohsinraza-fdev, would you like to continue working on this change?

@mohsinraza-fdev
Copy link
Contributor Author

(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 victorsanni marked this pull request as draft November 25, 2024 19:04
Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@flutter-dashboard
Copy link

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 package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@goderbauer
Copy link
Member

(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.

@goderbauer goderbauer closed this Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

6 participants