问题描述
框架和架构
我的 Flutter 应用程序中有一个特定的架构。我正在使用 BLoC 模式 (Flutter_bloc
) 来维护状态并从远程服务器获取数据。
自动完成的行为方式
我想构建自动完成输入。当用户输入时,它会在几毫秒后开始从服务器获取数据。作为用户类型,建议列表应从远程服务器更新并显示过滤值给用户。此外,如果存在这样的值 1,我需要设置自动完成文本字段的初始值。数据的呈现方式也是自定义的。建议列表向用户提供包含 name
和 id
值的建议,但文本字段只能包含 name
值(此 name
值也用于搜索建议)2。
我在使用 Flutter 材料库中的 RawAutocomplete
小部件时运气不佳。我已经通过利用 TextEditingController
和 didUpdateWidget
方法成功地使初始值出现在字段中。问题是,当我在该字段中输入时,正在获取建议并将其传递给小部件,但未构建建议列表(通过 optionsviewbuilder
构建)。如果我更改字段中的值,通常会显示该列表,但为时已晚,无法使用。
这是我尝试过的:
Link to live demo
注意:尝试输入“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,);
}));
}
}
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,));
}
}
预期行为
我希望每次有新建议可用时都会重新构建建议列表并将其提供给用户。
1 原因是输入应该向用户显示一个之前选择的值。该值也可能存储在设备上。所以输入要么是空的,要么是预填充的值。
2 这个例子是有限制的,但基本上文本字段不应包含与建议包含的特定原因相同的文本。
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)