Skip to content

Rebuild Autocomplete options at arbitrary time #159443

Description

@justinmc

Use case

When the user finishes typing in the field, a user of Autocomplete might make two network requests: one to quickly fetch some approximate options and another, longer request to get more detailed results. Autocomplete should be able to update the options with the result of each request. Currently the optionsViewBuilder is only called in response to the completion of the future returned by optionsBuilder.

Proposal

The hacky solution is probably to introduce a controller with a method to imperatively update the options: AutocompleteController.setOptions.

A better solution might require a breaking change. Really it seems like maybe the options should just be passed into Autocomplete as a parameter? That would also require the ability to listen to changes in the field.

Related

There are other request to be able to display things in the options view at certain times, for example while loading: #147377. Solving this issue would probably also fix that.

Activity

  1. added
    a: text inputEntering text in a text field or keyboard related problems
    on Nov 25, 2024
  2. justinmc commented on Dec 12, 2024

    @justinmc
    ContributorAuthor

    FYI @gnprice who brought this to my attention.

  3. added
    P2Important issues not at the top of the work list
    on Jan 16, 2025
  4. LongCatIsLooong commented on Feb 25, 2025

    @LongCatIsLooong
    Contributor

    I was creating a new issue for #157244 before I realize there's an existing issue,

    the use cases I have are similar:

    It seems Autocomplete.optionsBuilder is only called when the text editing controller's TextEditingValue changes. This does not work if optionsBuilder captures free variables, for example:

    optionsBuilder: (TextEditingValue v) => _database.lookup(v.text);

    if _database changes the optionsBuilder needs to be re-evaluated.

    Another scenario this fails is when the user rebuilds the Autocomplete widget with a different optionsBuilder function.

  5. gnprice commented on Jul 9, 2025

    @gnprice
    Member

    I sat down today and wrote some example code for this issue; see below.

    The repro is:

    1. Run the app, say on https://dartpad.dev/ .

    2. Click/tap the input field at the top, to focus it.

    3. Expected: The list of options appears.

    4. Actual: No options appear, even if you wait indefinitely.

    5. If you then start typing a query (like "a"), the options that match your query appear promptly. (*) If you delete what you've typed so the query is empty again, the full list of possible options appears. So everything works great after you first start typing something.

      (*) Unless you do so fast enough that the "network fetch" (the 100ms sleep, in the demo) to get the list of options hasn't finished yet. In that case it's the same situation as at the beginning.

    In Zulip, the main user-facing effect of this is here: zulip/zulip-flutter#1626. The user is reading a chat view with a bunch of previous messages in it to various different topics; they decide to compose a new message, and tap the text field to select what topic to send to; we want to show them an autocomplete of the existing topics; but currently that autocomplete doesn't appear unless they start typing. (Getting the full list of past topics in the channel involves a network request to the server.)

    Similarly if the user is in the middle of writing a message and they type "@", we want to show them an autocomplete of other users they can @-mention; but currently that autocomplete doesn't appear until they start typing part of the person's name to narrow it down. Very often the person they want to mention is one of the top handful we'd show without needing a query (e.g. it's one of the recent participants in that thread), so we'd like to not make them start typing the name.

    Example app
    import 'package:flutter/material.dart';
    
    void main() => runApp(const _MyApp());
    
    class _MyApp extends StatelessWidget {
      const _MyApp();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Autocomplete Demo',
          home: Scaffold(
            appBar: AppBar(title: Text('Autocomplete Demo')),
            body: const _MyHomePageBody()));
      }
    }
    
    class _MyHomePageBody extends StatefulWidget {
      const _MyHomePageBody();
    
      @override
      State<_MyHomePageBody> createState() => _MyHomePageBodyState();
    }
    
    class _MyHomePageBodyState extends State<_MyHomePageBody> {
      FocusNode focusNode = FocusNode();
      TextEditingController controller = TextEditingController();
      
      /// The list of possible options available for this autocomplete.
      ///
      /// Null until the actual list has been fetched from the network,
      /// or otherwise asynchronously obtained.
      List<String>? allOptions;
    
      /// The options from [allOptions] that match the user's current query.
      List<String> results = [];
    
      void initState() {
        super.initState();
        focusNode.addListener(_handleFocusChange);
        controller.addListener(_updateResults);
      }
      
      void _handleFocusChange() {
        if (allOptions == null) {
          // Only fetch the list of possible options when the input gets focus.
          // (In a real app, this might be because this is only part of the page
          // and the user might never interact with this input, so we don't want
          // to eagerly do a bunch of work that might not be needed.)
          _fetchOptions();
        }
      }
      
      Future<void> _fetchOptions() async {
        // In real life, imagine a network fetch, or a slow async computation.
        // For this demo, just sleep.
        await Future<void>.delayed(Duration(milliseconds: 100));
        allOptions = ['apple', 'banana', 'cucumber', 'durian'];
    
        // We have the list of possible options, so update the list of
        // matching options accordingly.
        _updateResults();
    
        // Alas, this doesn't cause the list to get shown in the UI!
        // Even if the whole widget tree gets rebuilt, [RawAutocomplete]
        // is keeping its own copy of the list of options; and it doesn't
        // update that copy until the text input changes.
        // So the user has to type something before they see any options.
      }
      
      void _updateResults() {
        final query = controller.text;
        // In real life the matching logic might be more complex
        // (and the options themselves might be more complex than strings);
        // but that doesn't interact with this issue.
        results = (allOptions ?? []).where(
          (option) => option.contains(query)).toList();
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(children: [
          TextButton(
            // This button is just for convenience in DartPad,
            // to be able to get focus back to the editor pane.
            onPressed: focusNode.unfocus,
            child: Text('Unfocus')),
          RawAutocomplete<String>(
            focusNode: focusNode,
            textEditingController: controller,
            fieldViewBuilder: (_, _, _, _) =>
              TextField(focusNode: focusNode, controller: controller),
            optionsBuilder: (query) => results,
            optionsViewBuilder: (_, _, _) {
              return Material(
                elevation: 4.0,
                child: ConstrainedBox(
                  constraints: const BoxConstraints(maxHeight: 300),
                  child: ListView.builder(
                    padding: EdgeInsets.zero,
                    shrinkWrap: true,
                    itemCount: results.length,
                    itemBuilder: (_, index) =>
                      InkWell(child: Text(results[index])))));
            }),
        ]);
      }
    }
  6. gnprice commented on Jul 10, 2025

    @gnprice
    Member

    A while ago, my colleague @chrisbobbe wrote up a draft for how to adapt the Autocomplete / RawAutocomplete API to resolve this issue: see branch here (or permalink).

    The core of it is that instead of optionsBuilder and onSelected, the RawAutocomplete widget takes as a parameter an instance of a new class RawAutocompleteController. That class extends ChangeNotifier; so the RawAutocompleteController implementation listens to the controller, and the app can update the list of options by calling a setter on the controller — very much like how a widget can control a text field via a TextEditingController.

    Copying here @chrisbobbe's original description of the branch:

    In my Flutter fork, [that draft branch] makes several NFC prep commits, then a larger commit that gives RawAutocomplete's caller the ability to control when its options-list state is updated. (That commit breaks lots of the public API, but it's probably possible in theory to offer the new API as an opt-in without breaking the current API.)

    Then my zulip-flutter branch wip-autocomplete-update-on-view-model uses that Flutter draft branch to demonstrate a fix for zulip/zulip-flutter#226 [i.e., the corresponding Zulip issue], and the tests pass.

    I wrote at the time a suggestion for how that draft could be adapted to preserve the existing API and avoid a breaking change:

    For making the API compatible — continuing to offer the existing API which is nice and convenient for the cases it works well for — I think the main tool I'd reach for is to have a separate constructor, along the lines of ListView.custom and the other ListView constructors.

    So there'd be a new RawAutocomplete.custom constructor which takes a controller, and then there'd be the existing constructor, and if you use the existing constructor then the state sets up a controller for you based on the optionsBuilder and onSelected and so on.

    That probably means a lot of the code that's deleted in your demo would just move onto a subclass (perhaps a private subclass) of RawAutocompleteController, which would get instantiated by default if you didn't provide a controller.

    That was in 2023 🙂, and we haven't yet found time to come back and turn that draft branch into a mergeable upstream PR. If anyone else would like to pick this up, they'd be very welcome to use that draft or any part of it as a starting point.

  7. gnprice commented on Jul 10, 2025

    @gnprice
    Member

    If anyone else would like to pick this up, they'd be very welcome to use that draft

    Here's also the same branch as a draft PR just now, in case that's a more convenient form to read it or use it:

  8. added a commit that references this issue on Mar 15, 2026
    7467c52
  9. plchekspend commented on Mar 25, 2026

    @plchekspend
  10. added 5 commits that reference this issue on Mar 31, 2026
    b6d9140
    1a2198e
    3b76fe2
    90c3cb5
    4d9a598
  11. added 4 commits that reference this issue on Apr 8, 2026
    e6490f7
    856069a
    a5fc3ba
    01aa1b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: text inputEntering text in a text field or keyboard related problemsteam-text-inputOwned by Text Input teamtriaged-text-inputTriaged by Text Input team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions