Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter custom FormField : validate and save methods are not called

Tags:

flutter

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

like image 251
manolo_el_muchacho Avatar asked Feb 24 '19 04:02

manolo_el_muchacho


People also ask

What does _formKey currentState save () do?

To save the data use _formKey. currentState. save() method which will call all the onSaved functions defined in TextFormFields .

How do you call a validator in Flutter?

You can use the _formKey. currentState() method to access the FormState , which is automatically created by Flutter when building a Form . The FormState class contains the validate() method. When the validate() method is called, it runs the validator() function for each text field in the form.


1 Answers

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
  }
like image 145
Ryan Remer Avatar answered Nov 15 '22 09:11

Ryan Remer