我对Flutter还是很陌生,我在创建自定义表单字段方面有些挣扎。问题是我的自定义FormField中的验证器和onSaved方法都没有被调用。对于触发formKey.currentState.validate()
或formKey.currentState.save()
时为什么忽略它们,我真的一无所知。
这是一个非常简单的窗口小部件,带有输入文本和按钮。 该按钮将获取用户的当前位置,并使用当前地址更新文本字段。 当用户在文本字段中输入地址时,它将在失去焦点时获取该地址的位置(我也已与Google Maps集成,但我简化了此操作以隔离问题)。
这是我的表单字段的构造函数:
class LocationFormField extends FormField<LocationData> {
LocationFormField(
{FormFieldSetter<LocationData> onSaved,
FormFieldValidator<LocationData> validator,
LocationData initialValue,
bool autovalidate = false})
: super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidate: autovalidate,
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
@override
FormFieldState<LocationData> createState() {
return _LocationFormFieldState();
}
}
由于我需要在自定义FormField中处理状态,因此我在FormFieldState对象中构建状态。按下按钮后,位置状态将更新:
class _LocationFormFieldState extends FormFieldState<LocationData> {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
focusNode: _addressInputFocusNode,
controller: _addressInputController,
decoration: InputDecoration(labelText: 'Address'),
),
SizedBox(height: 10.0),
FlatButton(
color: Colors.deepPurpleAccent,
textColor: Colors.white,
child: Text('Locate me !'),
onPressed: _updateLocation,
),
],
);
}
void _updateLocation() async {
print('current value: ${this.value}');
final double latitude = 45.632;
final double longitude = 17.457;
final String formattedAddress = await _getAddress(latitude, longitude);
print(formattedAddress);
if (formattedAddress != null) {
final LocationData locationData = LocationData(
address: formattedAddress,
latitude: latitude,
longitude: longitude);
_addressInputController.text = locationData.address;
// save data in form
this.didChange(locationData);
print('New location: ' + locationData.toString());
print('current value: ${this.value}');
}
}
这就是我在应用程序中实例化它的方式。这里没什么特别的;我将其放在带有表单键的表单中。还有另一个TextFormField可以验证它是否正常工作:
main.dart
Widget _buildLocationField() {
return LocationFormField(
initialValue: null,
validator: (LocationData value) {
print('validator location');
if (value.address == null || value.address.isEmpty) {
return 'No valid location found';
}
},
onSaved: (LocationData value) {
print('location saved: $value');
_formData['location'] = value;
},
); // LocationFormField
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
child: Column(
children: <Widget>[
_buildTitleTextField(),
SizedBox(
height: 10.0,
),
_buildLocationField(),
SizedBox(
height: 10.0,
),
_buildSubmitButton(),
],
),
),
),
),
),
);
}
由表单提交按钮触发的submit方法将仅尝试验证然后保存表单。
只需打印以以下格式保存的数据:
void _submitForm() {
print('formdata : $_formData');
if (!_formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
print('formdata : $_formData');
}
但是_formData['location']
始终返回null,并且永远不会调用验证器(日志中没有打印“验证器位置”或“保存的位置”)。
我创建了一个示例存储库来重现此问题。您可以尝试运行该项目,首先单击“查找我”!按钮,然后点击https://github.com/manumura/flutter-location-form-field
上的“保存”按钮答案 0 :(得分:1)
答案1:为构建器放置构建方法
替换FormField的生成器
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
使用您的自定义构建器功能
builder: (FormFieldState<LocationData> state) {
return Column(
children: <Widget>[
TextField(
focusNode: _addressInputFocusNode,
controller: _addressInputController,
decoration: InputDecoration(labelText: 'Address'),
),
SizedBox(height: 10.0),
FlatButton(
color: Colors.deepPurpleAccent,
textColor: Colors.white,
child: Text('Locate me !'),
onPressed: _updateLocation,
),
],
});
答案2:伪CustomFormFieldState
您无法扩展FormFieldState,因为覆盖“ build”功能会导致错误(如下所述)
但是,您可以创建一个以FormFieldState作为参数的小部件,使其成为一个单独的类来像扩展FormFieldState一样工作(对我来说,上述方法对我来说似乎更干净)
class CustomFormField extends FormField<List<String>> {
CustomFormField({
List<String> initialValue,
FormFieldSetter<List<String>> onSaved,
FormFieldValidator<List<String>> validator,
}) : super(
autovalidate: false,
onSaved: onSaved,
validator: validator,
initialValue: initialValue ?? List(),
builder: (state) {
return CustomFormFieldState(state);
});
}
class CustomFormFieldState extends StatelessWidget {
FormFieldState<List<String>> state;
CustomFormFieldState(this.state);
@override
Widget build(BuildContext context) {
return Container(), //The Widget(s) to build your form field
}
}
说明
扩展FormFieldState无效的原因是因为覆盖FormFieldState对象中的build方法会导致FormFieldState不向Form本身注册。
下面是我为了得到解释而遵循的功能列表
1)您的_LocationFormFieldState覆盖了build方法,这意味着FormFieldState的build方法永远不会执行
@override
Widget build(BuildContext context)
2)FormFieldState的构建方法将自身注册到当前FormState
///function in FormFieldState
Widget build(BuildContext context) {
// Only autovalidate if the widget is also enabled
if (widget.autovalidate && widget.enabled)
_validate();
Form.of(context)?._register(this);
return widget.builder(this);
}
3)然后,FormState将FormFieldState保存在列表中
void _register(FormFieldState<dynamic> field) {
_fields.add(field);
}
4)然后,当FormState保存/验证时,它将遍历FormFieldStates列表
/// Saves every [FormField] that is a descendant of this [Form].
void save() {
for (FormFieldState<dynamic> field in _fields)
field.save();
}
通过覆盖构建方法,您导致FormField不向Form注册,这就是为什么保存和加载Form不会调用自定义FormField的方法的原因。
如果FormState._register()方法是公开的而不是私有的,则可以在_LocationFormFieldStateState.build方法中调用此方法以将应用程序注册到表单,但是很遗憾,因为它是私有函数,所以不能。
还要注意,如果要在CustomFormFieldState的build方法中调用super.build()函数,则会导致StackOverflow
@override
Widget build(BuildContext context) {
super.build(context); //leads to StackOverflow!
return _buildFormField(); //anything you want
}
答案 1 :(得分:0)
有同样的问题。对我来说,当我更换时它起作用了
return state.build(state.context);
使用build方法中的实际代码,并从状态中删除了build方法覆盖。
答案 2 :(得分:0)
之所以发生这种情况,是因为您已覆盖build()
中的_LocationFormFieldState
方法。重写此方法时,用于注册自定义表单字段和表单验证的开箱即用机制也将被覆盖。因此,不会注册该字段,并且不会自动调用onSaved()
和validate()
方法。
_LocationFormFieldState
类中,将Widget build(BuildContext context)
方法的内容复制到一个新方法中。我们称之为Widget _constructWidget()
。由于我们处于有状态的类中,因此context
对象将隐式存在。Widget build(BuildContext context)
从_LocationFormFieldState
类中完全删除。由于我们已经删除了覆盖,因此将调用超类build
方法,该方法将为我们使用父表单注册此自定义表单字段。LocationFormFieldState
构造函数中,替换:builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
使用
builder: (FormFieldState<LocationData> state) {
return (state as _LocationFormFieldState)._constructWidget();
});
在这里,我们使用FormFieldState
运算符将_LocationFormFieldState
转换为as
,并调用了自定义_constructWidget()
方法,该方法返回窗口小部件树(以前在覆盖的{ {1}})
答案 3 :(得分:0)
对于所有像我一样迟到查看此问题的人,这里有一个实际的简单而干净的解决方案:
将您要验证和保存的所有文本字段放在 Form
小部件中,然后使用 GlobalKey
将所有字段一起保存和验证:
final _formKey = GlobalKey<FormState>();
//our save function that gets triggered when pressing a button for example
void save() {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
//add your code here
}
}
//code inside buildMethod:
Form(
key: _formKey,
child: TextFormField(
onSaved: (String value) { //code that gets executed with _formkey.currentState.save()
setState(() {
text = value;
});
},
),
)
此解决方案不会触发 StatefulWidget 不必要的重建,因为只有在用户通过按钮触发时才会更新值。