7

I'm trying to make an auto suggest input which fetches the results from a backend API. Here is my code:

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:hello_world/api.dart';
import 'package:hello_world/autocomplete_item.dart';

class Debouncer {
  final int milliseconds;

  VoidCallback? action;
  Timer? _timer;

  Debouncer({this.milliseconds = 250});

  run(VoidCallback action) {
    _timer?.cancel();
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }
}

class AutoCompleteInput extends StatefulWidget {
  const AutoCompleteInput({
    Key? key,
    this.label = 'Suggest',
    this.textInputAction,
    this.validator,
    this.errorMessage,
  }) : super(key: key);

  final String label;

  final TextInputAction? textInputAction;

  final FormFieldValidator<String>? validator;

  final String? errorMessage;

  @override
  _AutoCompleteInputState createState() => _AutoCompleteInputState();
}

class _AutoCompleteInputState extends State<AutoCompleteInput> {
  final _debouncer = Debouncer(milliseconds: 500);

  List<AutoCompleteItem> _options = [];

  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Autocomplete<AutoCompleteItem>(
      displayStringForOption: (AutoCompleteItem item) => item.value,
      optionsBuilder: _optionsBuilder,
      fieldViewBuilder: (
        BuildContext context,
        TextEditingController fieldTextEditingController,
        FocusNode fieldFocusNode,
        VoidCallback onFieldSubmitted,
      ) {
        return TextFormField(
          controller: fieldTextEditingController,
          focusNode: fieldFocusNode,
          autocorrect: false,
          maxLength: 50,
          maxLines: 1,
          keyboardType: TextInputType.text,
          textInputAction: widget.textInputAction,
          decoration: InputDecoration(
            labelText: widget.label,
            contentPadding: EdgeInsets.zero,
            errorText: widget.errorMessage,
            counterText: '',
            suffix: _isLoading
                ? Padding(
                    padding: const EdgeInsets.only(right: 1),
                    child: SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    ),
                  )
                : null,
          ),
          validator: widget.validator,
          onFieldSubmitted: (String value) {
            onFieldSubmitted();
          },
          onChanged: (text) {
            _debouncer.run(() async {
              setState(() => _isLoading = true);

              await _fetchResults(text);

              setState(() => _isLoading = false);
            });
          },
        );
      },
      optionsViewBuilder: (
        BuildContext context,
        AutocompleteOnSelected<AutoCompleteItem> onSelected,
        Iterable<AutoCompleteItem> options,
      ) {
        return Align(
          alignment: Alignment.topLeft,
          child: Material(
            elevation: 2,
            child: Container(
              width: 300,
              constraints: BoxConstraints(maxHeight: 250),
              child: ListView.builder(
                padding: EdgeInsets.all(10.0),
                itemCount: options.length,
                shrinkWrap: true,
                itemBuilder: (BuildContext context, int index) {
                  final AutoCompleteItem option = options.elementAt(index);

                  return GestureDetector(
                    onTap: () {
                      onSelected(option);
                    },
                    child: ListTile(
                      dense: true,
                      title: Text(option.value),
                    ),
                  );
                },
              ),
            ),
          ),
        );
      },
      onSelected: (AutoCompleteItem selection) {
        print('${selection.type} => ${selection.value}');
      },
    );
  }

  Future<void> _fetchResults(String text) async {
    // WE PERFORM THE HTTP REQUEST HERE AND THEN ASSIGN THE RESPONSE TO `_options`

    setState(() async {
      _options = await Api.fetchSuggestions(text);
    });
  }

  Iterable<AutoCompleteItem> _optionsBuilder(
      TextEditingValue textEditingValue) {
    if (textEditingValue.text == '') {
      return const Iterable<AutoCompleteItem>.empty();
    }
    return _options;
  }
}

As you can see, I call _fetchResults method (which fetches the data from the API) inside the onChanged method of TextFormField. But when the data is fetched and the state is updated, there is no overlay of suggestions! I checked the length of _options and it has all of the results but still setState did not forced Autocomplete widget to rebuild it's overlay. Although when I remove the last character of the TextFormField, it immediately shows the overlay. How can I fix this issue?

Alireza Beitari
  • 436
  • 4
  • 15

2 Answers2

0

In the TextField's onChanged method calls the textEditingController.notifyListeners() and ignores the warnings

example

TextField(
    onChanged: (text)async{
            timer?.cancel();
            timer = Timer(const Duration(milliseconds: 700), () async {
            await getData(textEditingController).whenComplete((){
              setState(() {
                // ignore: invalid_use_of_protected_member 
                //invalid_use_of_visible_for_testing_member
                textEditingController.notifyListeners();
              });
            });
          });
          },
         )
Orlindo
  • 3
  • 5
0

Actually the setState within the TextField's onChanged won't trigger the optionsBuilder callback. Instead you should use directly the optionsBuilder to fetch await and return the updated List. Forget about using a stateful list as source for the optionsBuilder, that won't work for now.

You will need to:

  1. Move the TextField's onChanged function to the _optionsBuilder method.
  2. Make the _optionsBuilder method async.
  3. Directly return the fetched List within _optionsBuilder instead of relying on the stateful _options field.
caiohamamura
  • 2,260
  • 21
  • 23