Flutter:建议列表更改时自动完成不显示建议

use*_*783 12 autocomplete widget flutter

框架与架构

我的 Flutter 应用程序中有一个特定的架构。我正在使用 BLoC 模式 ( flutter_bloc) 来维护状态并从远程服务器获取数据。

自动完成应该如何表现

我想构建自动完成输入。当用户键入时,它会在几毫秒后开始从服务器获取数据。当用户键入时,应从远程服务器更新建议列表,并向用户显示过滤后的值。此外,如果存在1 ,我需要设置自动完成文本字段的初始值。数据的呈现方式也是定制的。建议列表向用户提供包含nameid值的建议,但文本字段只能包含name值(该name值也用于搜索建议)2

使用 Flutter 材质库中的小部件时,我运气不佳RawAutocompleteTextEditingController我通过杠杆和方法,成功地让初始价值出现在现场didUpdateWidget。问题是,当我在字段中输入时,建议将被获取并传递到小部件,但建议列表(通过 构建optionsViewBuilder)并未构建。通常,如果我更改该字段中的值,该列表就会出现,但为时已晚,无用。

这是我尝试过的:

链接到现场演示

注意:尝试输入“xyz”,该模式应与建议之一匹配。稍等片刻并删除单个字符将显示建议。

我附上两个组件作为示例。调用的父组件DetailPage负责触发建议的获取,并存储选定的建议/输入值。子组件DetailPageForm包含实际输入。该示例是人为限制的,但它位于常规MaterialApp父窗口小部件中。为简洁起见,我不包含BLoC代码,仅使用常规流。该代码运行良好,我专门为此示例创建了它。

DetailPage

import 'dart:async';
import 'package:flutter/material.dart';

import 'detail_page_form.dart';

@immutable
class Suggestion {
  const Suggestion({
    this.id,
    this.name,
  });

  final int id;
  final String name;
}

class MockApi {
  final _streamController = StreamController<List<Suggestion>>();

  Future<void> fetch() async {
    await Future.delayed(Duration(seconds: 2));
    _streamController.add([
      Suggestion(id: 1, name: 'xyz'),
      Suggestion(id: 2, name: 'jkl'),
    ]);
  }

  void dispose() {
    _streamController.close();
  }

  Stream<List<Suggestion>> get stream => _streamController.stream;
}

class DetailPage extends StatefulWidget {
  final _mockApi = MockApi();

  void _fetchSuggestions(String query) {
    print('Fetching with query: $query');
    _mockApi.fetch();
  }

  @override
  _DetailPageState createState() => _DetailPageState(
        onFetch: _fetchSuggestions,
        stream: _mockApi.stream,
      );
}

class _DetailPageState extends State<DetailPage> {
  _DetailPageState({
    this.onFetch,
    this.stream,
  });

  final OnFetchCallback onFetch;
  final Stream<List<Suggestion>> stream;
  /* NOTE: This value can be used for initial value of the
           autocomplete input
  */
  Suggestion _value;

  _handleSelect(Suggestion suggestion) {
    setState(() {
      _value = suggestion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Detail')),
        body: StreamBuilder<List<Suggestion>>(
            initialData: [],
            stream: stream,
            builder: (context, snapshot) {
              if (snapshot.hasError) {
                return Container(
                    padding: const EdgeInsets.all(10.0),
                    decoration: BoxDecoration(
                        color: Colors.red,
                        ),
                    child: Flex(
                        direction: Axis.horizontal,
                        children: [ Text(snapshot.error.toString()) ]
                        )
                    );
              }

              return DetailPageForm(
                  list: snapshot.data,
                  value: _value != null ? _value.name : '',
                  onSelect: _handleSelect,
                  onFetch: onFetch,
                  );
            }));
  }
}
Run Code Online (Sandbox Code Playgroud)

DetailPageForm

import 'dart:async';

import 'package:flutter/material.dart';

import 'detail_page.dart';

typedef OnFetchCallback = void Function(String);
typedef OnSelectCallback = void Function(Suggestion);

class DetailPageForm extends StatefulWidget {
  DetailPageForm({
    this.list,
    this.value,
    this.onFetch,
    this.onSelect,
  });

  final List<Suggestion> list;
  final String value;
  final OnFetchCallback onFetch;
  final OnSelectCallback onSelect;

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

class _DetailPageFormState extends State<DetailPageForm> {
  Timer _debounce;
  TextEditingController _controller = TextEditingController();
  FocusNode _focusNode = FocusNode();
  List<Suggestion> _list;

  @override
  void initState() {
    super.initState();
    _controller.text = widget.value ?? '';
    _list = widget.list;
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  void didUpdateWidget(covariant DetailPageForm oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.value != widget.value) {
      _controller = TextEditingController.fromValue(TextEditingValue(
          text: widget.value,
          selection: TextSelection.fromPosition(TextPosition(offset: widget.value.length)),
          ));
    }

    if (oldWidget.list != widget.list) {
      setState(() {
        _list = widget.list;
      });
    }
  }

  void _handleInput(String value) {
    if (_debounce != null && _debounce.isActive) {
      _debounce.cancel();
    }

    _debounce = Timer(const Duration(milliseconds: 300), () {
      widget.onFetch(value);
    });
  }

  @override
  Widget build(BuildContext context) {
    print(_list);
    return Container(
        padding: const EdgeInsets.all(10.0),
        child: RawAutocomplete<Suggestion>(
          focusNode: _focusNode,
          textEditingController: _controller,
          optionsBuilder: (TextEditingValue textEditingValue) {
            return _list.where((Suggestion option) {
              return option.name
                  .trim()
                  .toLowerCase()
                  .contains(textEditingValue.text.trim().toLowerCase());
            });
          },
          fieldViewBuilder: (BuildContext context,
              TextEditingController textEditingController,
              FocusNode focusNode,
              VoidCallback onFieldSubmitted) {
            return TextFormField(
              controller: textEditingController,
              focusNode: focusNode,
              onChanged: _handleInput,
              onFieldSubmitted: (String value) {
                onFieldSubmitted();
              },
            );
          },
          optionsViewBuilder: (context, onSelected, options) {
            return Align(
              alignment: Alignment.topLeft,
              child: Material(
                elevation: 4.0,
                child: SizedBox(
                  height: 200.0,
                  child: ListView.builder(
                    padding: const EdgeInsets.all(8.0),
                    itemCount: options.length,
                    itemBuilder: (BuildContext context, int index) {
                      final option = options.elementAt(index);
                      return GestureDetector(
                        onTap: () {
                          onSelected(option);
                        },
                        child: ListTile(
                          title: Text('${option.id} ${option.name}'),
                        ),
                      );
                    },
                  ),
                ),
              ),
            );
          },
          onSelected: widget.onSelect,
        ));
  }
}
Run Code Online (Sandbox Code Playgroud)

在图像的最后,您可以看到我必须删除一个字母才能显示建议。

Flutter 应用程序示例图像

预期行为

我希望每次有新建议可用时都会重新构建建议列表并将其提供给用户。


1原因是输入应向用户显示之前选择的值。该值也可能存储在设备上。因此输入要么是空的,要么是预先填充了值的。
2此示例受到限制,但基本上文本字段不应包含与建议包含的文本相同的文本(出于特定原因)。

小智 6

当我将建议设置为状态时,我通过调用 TextEditingController 上存在的 notificationListeners 方法解决了这个问题。

  setState(() {
    _isFetching = false;
    _suggestions = suggestions.sublist(0, min(suggestions.length, 5));
    _searchController.notifyListeners();
  });
Run Code Online (Sandbox Code Playgroud)

linter 确实说我应该在 Widget 上实现 ChangeNotifier 类,但在本例中我不必这样做,没有它它也能工作。


Gyu*_*hoi 0

移动你的_handleInput内部,optionsBuilder因为后者首先被调用。

          optionsBuilder: (TextEditingValue textEditingValue) {
            _handleInput(textEditingValue.text);  // await if necessary 
            return _list.where((Suggestion option) {
              return option.name
                  .trim()
                  .toLowerCase()
                  .contains(textEditingValue.text.trim().toLowerCase());
            });
          },
Run Code Online (Sandbox Code Playgroud)