I am quite new to Flutter, and I am struggling a bit to create a custom Form Field. The issue is that neither the validator nor the onSaved method from my custom FormField are called. I really am clueless on why they get ignored when I trigger a formKey.currentState.validate() or formKey.currentState.save().
This is a pretty simple widget for now, with an input text and a button.
The button will fetch the current location of the user, and update the text field with the current address.
When the user inputs an address in the text field, it will fetch the location for that address on focus lost (I have also integration with Google Maps, but I simplified it to isolate the issue).
Here is the constructor of my form field :
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();
}
}
As I need to handle state in my custom FormField, I build it in the FormFieldState object. The location state is updated when the button is pressed :
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}');
}
}
This is how I instantiate it in my app. Nothing special here; I put it in a Form with a form key. There is another TextFormField to verify that this one is working fine:
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(),
],
),
),
),
),
),
);
}
The submit method triggered by the form submit button will just try to validate then save the form.
Just printing the data saved in the form:
void _submitForm() {
print('formdata : $_formData');
if (!_formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
print('formdata : $_formData');
}
But _formData['location'] always returns null, and the validator is never called (no 'validator location' or 'location saved' printed in logs).
I created a sample repo to reproduce this issue. You can try running the project, click first on the Locate me ! button, then the Save button at https://github.com/manumura/flutter-location-form-field
Answer 1: Put the build method for the Builder
Replace the FormField's builder
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
with your custom builder function
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,
),
],
});
Answer 2: Pseudo CustomFormFieldState
You can't extend the FormFieldState because overriding the "build" function causes errors (explained below)
However you can create a Widget that takes the FormFieldState as a parameter to make it a separate class to act like it extends the FormFieldState (which seems a bit cleaner to me then the above method)
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
}
}
Explanation
The reason why extending the FormFieldState doesn't work is because overriding the build method in the FormFieldState object causes the FormFieldState to not be registered with the Form itself.
Below is a list of functions that I followed to get my explanation
1) Your _LocationFormFieldState overrides the build method which means the build method of the FormFieldState never executes
#override
Widget build(BuildContext context)
2) The build method the FormFieldState registers itself to the current 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) The FormState then saves the FormFieldState in a List
void _register(FormFieldState<dynamic> field) {
_fields.add(field);
}
4) Then when the FormState saves/validates it loops through the list of FormFieldStates
/// Saves every [FormField] that is a descendant of this [Form].
void save() {
for (FormFieldState<dynamic> field in _fields)
field.save();
}
By overriding the build method you cause the FormField to not be registered with the Form, which is why saving and loading the Form doesn't call the methods of your custom FormField.
If the FormState._register() method was public instead of private you could call this method in your _LocationFormFieldState.build method to register your app to the form, but sadly since it is a private function you cannot.
Also note that if you were to call the super.build() function in your CustomFormFieldState's build method it leads to a StackOverflow
#override
Widget build(BuildContext context) {
super.build(context); //leads to StackOverflow!
return _buildFormField(); //anything you want
}
This is happening because you have overridden the build() method in _LocationFormFieldState. When you override this method, out of the box mechanism for registering the custom form field & validation with the form is also overridden. Therefore, the field is not getting registered and the onSaved()and validate() methods are not called automatically.
In your _LocationFormFieldState class, copy the contents of the Widget build(BuildContext context) methods into a new method. Let's call it Widget _constructWidget(). Since, we are in a stateful class, the context object will be implicitly present.
Next, remove the Widget build(BuildContext context) entirely from the _LocationFormFieldState class. Since we have removed the override, the superclass build method will be called, which will do the registration of this custom form field with the parent form for us.
Now, in the LocationFormFieldState constructor, replace:
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
With
builder: (FormFieldState<LocationData> state) {
return (state as _LocationFormFieldState)._constructWidget();
});
Here, we typecasted the FormFieldState to _LocationFormFieldState using the as operator and called our custom _constructWidget() method which returns the widget tree (which was previously in the overridden build())
Had the same problem. For me it worked when I replaced
return state.build(state.context);
With actual code from the build method and removed the build method override from the state.
For all viewing this question late like me, here's an actual simple and clean solution:
Put all the TextFields you want to validate and save inside a Form Widget and then use a GlobalKey to save and validate all fields together:
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;
});
},
),
)
This solution won't trigger unwanted rebuilds of the StatefulWidget because the values only get updated once the user triggers via the button.
Related
I am building an app that generates form fields from JSON data.
the Form widget is declared in the custom_form.dart
child: Form(
key: _globalKey,
child: Column(
children: [
SizedBox(
height: 10,
),
Text("Register Form: "),
SizedBox(
height: 3,
),
for (var i = 0; i < this.widget.widgetList.length; i++)
generateWidgetFromType(this.widget.widgetList[i]),
I'm using that generateWidgetFrom Type to generate the widgets
Widget generateWidgetFromType(Mywidget mywidget) {
switch (mywidget.type) {
case "CheckBox":
return GenerateCheckBox(mywidget);
case "Input":
return CustomGeneratedWidget(mywidget,
onSaved: (value) => widget.test = value);
case "DatePicker":
return GenerateDatePicker(mywidget);
}
return Text("This type is not supported yet");
}
The TextFormField is declared in the CustomGeneratedWidget
child: TextFormField(
controller: textEditingController,
obscureText: isPasswordField(this.widget.mywidget.key),
keyboardType: customType(this.widget.mywidget.key),
decoration: InputDecoration(
labelText: this.widget.mywidget.label,
border: OutlineInputBorder()),
validator: (value) {
I want to retrieve data from TextFormField in the CustomGeneratedWidget when I click on the Add button in the custom_form.dart
One way might be to add a method called getValue() on to your CustomGeneratedWidget class:
class CustomGeneratedWidget extends Mywidget {
TextEditingController textEditingController = TextEditingController();
...
String getValue() {
return textEditingController.text;
}
...
#override
Widget build(BuildContext context) {
...
}
}
I'm guessing, based on what you're building, that you'll want to eventually get data not just from TextFormField / CustomGeneratedWidget instances, but also instances of GenerateCheckBox and GenerateDatePicker as well. With that being the case, if it were me, I would add an abstract method to Mywidget that you override in its subclasses, perhaps called getValue(). So, Mywidget might look like:
abstract class Mywidget extends Widget {
...
// this method is purposefully unimplemented, and is therefore an "abstract" method
dynamic getValue();
...
}
In your subclasses, such as CustomGeneratedWidget, you would "override" this method by redefining it / implementing it, and therefore making it "concrete":
class CustomGeneratedWidget extends Mywidget {
TextEditingController textEditingController = TextEditingController();
...
#override
String getValue() {
return textEditingController.text;
}
#override
Widget build(BuildContext context) {
return Container(
child: TextFormField(
controller: textEditingController
...
)
);
}
}
Back in custom_form.dart, in your handler for your 'Add' button, you could go through the list of created Mywidget instances and call getValue() on each.
How to stick validation text inside TextFormField. By default it goes below
the text field
I want like this
But it shows like this
I have looked in various sources, but did not find a suitable answer
is there any way to show the validation text inside the textbox
You can update text from TextEditingController if validation fails for a certain text field and also can remove text from controller in "onTap" property.
TextEditingController _passwordController = TextEditingController();
if(condition)
{
//success call
}
else
{
setState((){
_passwordController.text="Password Does not match
});
}
you should validate your form in the Following way
class MyForm extends StatefulWidget {
#override
MyFormState createState() {
return MyFormState();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class MyFormState extends State<MyForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyFormState>.
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
// sendData();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Processing Data')));
}
},
child: Text('Submit'),
),
),
],
),
);
}
}
I created a slider-based stepper form using TabBarView which validate the input before switching. It works, but when I go back, the state was reset. This behavior leads me to an empty form when I try to collect the data at the end of the tab.
I have googled for few hours and have been tried switching the current GetView<MyController> to the classic StatefulWidget with AutomaticKeepAliveMixin with no luck, so I revert it.
I'm a bit stuck, I wonder if there is any other way to achieve this, the GetX way, if possible.
visual explanation
`
create_account_form_slider.dart
class CreateAccountFormSlider extends GetView<CreateAccountController> {
#override
Widget build(BuildContext context) {
return Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: controller.tabController,
children: [
_buildEmailForm(),
_buildNameForm(),
_buildPasswordForm(),
],
),
);
}
Widget _buildEmailForm() {
return Form(
key: controller.emailFormKey,
child: Column(
children: [
Spacer(), // Necessary to push the input to the bottom constraint, Align class doesn't work.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
margin: EdgeInsets.zero,
label: 'create_account_form_email'.tr,
hintText: 'janedoe#example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
),
),
],
),
);
}
... each form has similar structure (almost identical), so i will not include it here
create_account_controller.dart
class CreateAccountController extends GetxController
with SingleGetTickerProviderMixin {
final tabIndex = 0.obs;
final emailFormKey = GlobalKey<FormState>();
FormState get emailForm => emailFormKey.currentState;
final emailFocusNode = FocusNode();
final email = ''.obs;
TabController tabController;
#override
void onInit() {
_initTabController();
super.onInit();
}
#override
void onClose() {
_disposeFocusNodes();
_disposeTabController();
super.onClose();
}
/// Initialize tab controller and add a listener.
void _initTabController() {
tabController = TabController(vsync: this, length: 3);
tabController.addListener(_tabListener);
}
/// Listen on tab change and update `tabIndex`
void _tabListener() => tabIndex(tabController.index);
/// Dispose tab controller and remove its listener.
void _disposeTabController() {
tabController.removeListener(_tabListener);
tabController.dispose();
}
/// Dispose all the focus nodes.
void _disposeFocusNodes() {
emailFocusNode.dispose();
}
/// Animate to the next slide.
void _nextSlide() => tabController.animateTo(tabIndex() + 1);
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
_nextSlide();
return focusScope.requestFocus(nameFocusNode);
}
...
}
/// A function that checks the validity of the given value.
///
/// When the email is empty, show required error message and when the email
/// is invalid, show the invalid message.
String emailValidator(String val) {
if (val.isEmpty) return 'create_account_form_error_email_required'.tr;
if (!val.isEmail) return 'create_account_form_error_email_invalid'.tr;
return null;
}
/// Submit data to the server.
void _submit() {
print('TODO: implement submit');
print(email());
}
}
I made it by saving the form and adding an initialValue on my custom FormInput widget then put the observable variable onto each related FormInput. No need to use keepalive mixin.
create_account_controller.dart
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
// save the form so the value persisted into the .obs variable
emailForm.save();
// slide to next form
_nextSlide();
// TODO: wouldn't it be nice if we use autofocus since we only have one input each form?
return focusScope.requestFocus(nameFocusNode);
}
...
}
create_account_form_slider.dart
Obx( // wrap the input inside an Obx to rebuild with the new value
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
label: 'create_account_form_email'.tr,
hintText: 'janedoe#example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
initialValue: controller.email(), // use initial value to keep current value when user go back from the next slide
onSaved: controller.email, // persist current value into the .obs variable
),
),
),
FYI: The FormInput is just a regular TextInput, only decoration is modified. This should work with the regular flutter TextInput.
if you want to use AutomaticKeepAliveMixin in GetX like StatefulWidget. You can add the parameter 'permanent: true' in Get.put like this
Get.put<HomeController>(
HomeController(),
permanent: true,
);
Full code on HomeBinding like this
import 'package:get/get.dart';
import '../controllers/home_controller.dart';
class HomeBinding extends Bindings {
#override
void dependencies() {
Get.put<HomeController>(
HomeController(),
permanent: true,
);
}
}
I wish to validate if the values within 2 TextFormFields matches.
I could validate them individually.
But how could I capture both those values to validate by comparing?
import 'package:flutter/material.dart';
class RegisterForm extends StatefulWidget {
#override
_RegisterFormState createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20),
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'Confirm Password'
),
validator: (value) {
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
RaisedButton(
onPressed: (){
if (_formKey.currentState.validate()) {
print('ok');
} else {
print('not ok');
}
},
),
],
),
)
],
),
);
}
}
One possible solution as follows.
I could store them as values within _RegisterFormState and retrieve them within the validate blocks. But is there a cleaner way to achieve this?
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
String password;
String confirmPassword;
.....
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
setState(() {
password = value;
});
performValidation(password, confirmPassword); // some custom validation method
return 'is empty';
}
return value;
},
),
.....
}
P.S: If there would be a better way to do it via a state management tool, I am using Provider. Not looking for Bloc solutions.
There are few steps I have performed to achieve this. You can refer that to achieve your goal.
Create a stateful widget and return a InputBox from there
add a property named as callback and set it's datatype as
ValueSetter callback;
assign this callback to onChanged Event of the input box
onChanged: (text) {
widget.callback(text);
},
Use your custom widget in the class where you want to use the input box
while using your widget pass a callback to it
InputWithLabel(
callback: (value) {
password = value;
},
),
InputWithLabel(
callback: (value) {
confirmPassword = value;
},
),
and at last, we have to compare those values,
you can bind a key to your form add use it in saved event of it. You can wrap it in Button's callback
if (InputValidation.validatePasswordandConfirm(password, cpassword)) {
// your after form code
}
To have it a real time comparison mark one of the input's callback in setState(){} and create a new property in your Custom Text Widget named as compareTxt;
and on validator check for compare text and return the error message
validator: (text) {
if (widget.comaparetext != text) {
return 'Password does not match';
}
I had the same problem but I was using custom TextFormField widgets.
So this is how I achieved it:
Problem:
I had two TextFormField widgets in a Form and I wanted to compare the values
of those two TextFormWidgets, to check whether the value of one filed is greater
then the other.
Solution:
I defined two controllers globally, because the TextFormWidgets were in a separate class.
final _startingRollNumberController = TextEditingController();
final _endingRollNumberController = TextEditingController();
Passed these controllers to the instances of TextFormField in the Form class.
CustomTextField('Starting R#', _startingRollNumberController),
CustomTextField('Ending R#', _endingRollNumberController),
And validated them in the TextFormField class.
class CustomTextField extends StatelessWidget {
// *===== CustomTextField class =====* //
CustomTextField(this._hintText, this._controller);
final String _hintText;
final _controller;
#override
Widget build(BuildContext context) {
return TextFormField(
validator: (value) {
final _startingRollNumber = _startingRollNumberController.text;
final _endingRollNumber = _endingRollNumberController.text;
// Starting roll-number needs to be smaller then the ending roll-number.
if (!_startingRollNumber.isEmpty && !_endingRollNumber.isEmpty) {
if (int.parse(_startingRollNumber) >= int.parse(_endingRollNumber)) {
return 'starting roll number must be smaller.';
}
}
},
);
}
}
This may not be the best approach but this is how I achieved it. There is
also, this flutter_form_builder package which can be used to achieve this easily using the form key.
A related question: Flutter: Best way to get all values in a form
I like to check if an email already exists in the database on my backend. Therefore I tried to use a state var which should be changed after the async call returns. I found the following threads which contains accepted answeres.
Flutter - Async Validator of TextFormField
Flutter firebase validation of form field inputs
I tried these answeres and also some variations but it still doesn't work for me. I just simulate the backend call. Setting _emailExist to true is printed but I don't see any error. If I click the button twice, the error message is shown correctly.
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
LoginPage({Key key}) : super(key: key);
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
bool _emailExist = false;
#override
initState() {
super.initState();
}
checkEmail(String name) {
// Simulare async call
Future.delayed(Duration(seconds: 2)).then((val) {
setState(() {
_emailExist = true;
});
print(_emailExist);
});
return _emailExist;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Container(
child: SingleChildScrollView(
child: Form(
key: _loginFormKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (value) =>
checkEmail(value) ? "Email already taken" : null,
),
RaisedButton(
child: Text("Login"),
onPressed: () {
if (_loginFormKey.currentState.validate()) {}
},
)
],
),
))));
}
}
TextFormField expects a synchronous function as a validator (that's an ordinary function that does some task and then returns its result).
checkEmail is exactly this, a synchronous function. It sets up a Future which will, in two seconds, set _emailExist to true. But crucially, it doesn't then wait around for two seconds. It immediately returns the current value of _emailExist (which is false the first time it runs). Two seconds later, your Future resolves and sets _emailExist to true. That's why, when you run it the second time, it works as expected (_checkEmail again returns the current value of _emailExist but this is now true).
One way around this is to provide onChanged and decoration arguments to achieve the same effect:
TextFormField(
onChanged: _handleChange,
decoration: InputDecoration(
errorText: _emailExist ? "Email already taken" : null,
),
),
Now, you can make your asynchronous backend call as the text field changes, and then update state depending on the response:
void _handleChange(String val) {
Future.delayed(Duration(seconds: 2)).then(() {
setState(() {
_emailExist = true;
});
print(_emailExist);
});
}
It's a good idea to debounce this function so you're not sending off a request with every letter the user types!
Here's a good video and some docs on asynchronous coding in Flutter/Dart.