1

I am using stepper widget, and using a Form widget in each step of stepper with different form keys in order to validate each step ... the problem is that this._formKey.currentState.validate() always returns false even if the validation criteria fulfills for the current step ... following is the code that i use to build individual steps in stepper:

  Widget _buildCustomerInfoWidget() {
    return Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            CustomTextField(
              focusNode: _focusNodeID,
              hintText: Translations.of(context).cnic,
              labelText: Translations.of(context).cnic,
              controller: _controllerIdentifier,
              keyboardType: TextInputType.number,
              hasError: _isIdentifierRequired,
              validator: (String t) => _validateIdentifier(t),
              maxLength: 13,
            ),
            CustomTextField(
              focusNode: _focusNodeAmount,
              hintText: Translations.of(context).amount,
              labelText: Translations.of(context).amount,
              controller: _controllerAmount,
              keyboardType: TextInputType.number,
              hasError: _isAmountRequired,
              validator: (String t) => _validateAmount(t),
              maxLength: 6,
            ),
          ],
        ));
  }

  Widget _buildCustomerVerificationWidget() {
    return Form(
        key: _formKeyOTP,
        child: Column(
          children: <Widget>[
            Center(
                child: Padding(
                    padding:
                        EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
                    child: Text(
                      Translations.of(context).helpLabelOTP,
                      style: new TextStyle(
                          color: Theme.of(context).primaryColor,
                          fontStyle: FontStyle.italic),
                    ))),
            CustomTextField(
              focusNode: _focusNodeOTP,
              hintText: Translations.of(context).otp,
              labelText: Translations.of(context).otp,
              controller: _controllerOTP,
              keyboardType: TextInputType.number,
              hasError: _isAmountRequired,
              validator: (String t) => _validateOTP(t),
              maxLength: 4,
              obscureText: true,
            ),
          ],
        ));
  }

  List<Step> _buildStepperSteps(BuildContext context) {
    List<Step> paymentSteps = [
      Step(
          title: Text(
            Translations.of(context).infoStepLabel,
          ),
          content: _buildCustomerInfoWidget(),
          state: StepState.indexed,
          isActive: _isStepActive),
      Step(
          title: Text(Translations.of(context).submitStepLabel),
          state: StepState.indexed,
          content: _buildCustomerVerificationWidget(),
          isActive: !_isStepActive),
    ];

    return paymentSteps;
  }

Complete code for reference:

class PullPaymentPage extends StatefulWidget {
  PullPaymentPage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  State<StatefulWidget> createState() {
    return PullPaymentState();
  }
}

class PullPaymentState extends State<PullPaymentPage> {
  final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
  final GlobalKey<FormState> _formKeyOTP = new GlobalKey<FormState>();
  final TextEditingController _controllerIdentifier =
      new TextEditingController();
  final FocusNode _focusNodeOTP = new FocusNode();
  final FocusNode _focusNodeID = new FocusNode();
  final FocusNode _focusNodeAmount = new FocusNode();
  final TextEditingController _controllerAmount = new TextEditingController();
  final TextEditingController _controllerOTP = new TextEditingController();

  bool _isIdentifierRequired = false;
  bool _isAmountRequired = false;
  bool _isOTPRequired = false;
  bool _isStepActive = true;
  int _currentStep = 0;


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: _buildAppBar(context),
        body: new Builder(builder: (BuildContext context) {
          return Container(child: _buildStepper(context));
        }));
  }

  Stepper _buildStepper(BuildContext context) {
    final Map functionMap = {0: _sendOTP, 1: _pullPayment};

    List<Step> paymentSteps = _buildStepperSteps(context);

    return Stepper(
      steps: paymentSteps,
      type: StepperType.vertical,
      currentStep: this._currentStep,
      onStepTapped: (step) {
        setState(() {
          if (step != 1) {
            _currentStep = step;
          }
          (_currentStep == 0) ? _isStepActive = true : _isStepActive = false;
        });
      },
      onStepContinue: () {
        functionMap[_currentStep]().then((value) {
          if (value == true) {
            setState(() {
              _currentStep++;
            });
            if (_currentStep == 1) {
              Scaffold.of(context).showSnackBar(SnackBar(
                    content: Text(Translations.of(context).otpSuccess),
                    duration: Duration(seconds: 4),
                  ));
            }
          }
        });
      },
      onStepCancel: () {
        setState(() {
          _currentStep == 0 ? Navigator.of(context).pop() : stepBack();
        });
      },
    );
  }

  void stepBack() {
    _isStepActive = true;
    _currentStep--;
  }

  Map<int, Function> createFunctionMap() {
    Map functionMap = {1: _sendOTP(), 2: _pullPayment()};
    return functionMap;
  }

  AppBar _buildAppBar(BuildContext context) {
    return AppBar(
      title: Text(
        Translations.of(context).pullPayment,
      ),
      backgroundColor: Theme.of(context).primaryColorDark,
      leading: new BackButton(),
    );
  }

  Widget _buildCustomerInfoWidget() {
    return Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            CustomTextField(
              focusNode: _focusNodeID,
              hintText: Translations.of(context).cnic,
              labelText: Translations.of(context).cnic,
              controller: _controllerIdentifier,
              keyboardType: TextInputType.number,
              hasError: _isIdentifierRequired,
              validator: (String t) => _validateIdentifier(t),
              maxLength: 13,
            ),
            CustomTextField(
              focusNode: _focusNodeAmount,
              hintText: Translations.of(context).amount,
              labelText: Translations.of(context).amount,
              controller: _controllerAmount,
              keyboardType: TextInputType.number,
              hasError: _isAmountRequired,
              validator: (String t) => _validateAmount(t),
              maxLength: 6,
            ),
          ],
        ));
  }

  Widget _buildCustomerVerificationWidget() {
    return Form(
        key: _formKeyOTP,
        child: Column(
          children: <Widget>[
            Center(
                child: Padding(
                    padding:
                        EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
                    child: Text(
                      Translations.of(context).helpLabelOTP,
                      style: new TextStyle(
                          color: Theme.of(context).primaryColor,
                          fontStyle: FontStyle.italic),
                    ))),
            CustomTextField(
              focusNode: _focusNodeOTP,
              hintText: Translations.of(context).otp,
              labelText: Translations.of(context).otp,
              controller: _controllerOTP,
              keyboardType: TextInputType.number,
              hasError: _isAmountRequired,
              validator: (String t) => _validateOTP(t),
              maxLength: 4,
              obscureText: true,
            ),
          ],
        ));
  }

  List<Step> _buildStepperSteps(BuildContext context) {
    List<Step> paymentSteps = [
      Step(
          title: Text(
            Translations.of(context).infoStepLabel,
          ),
          content: _buildCustomerInfoWidget(),
          state: StepState.indexed,
          isActive: _isStepActive),
      Step(
          title: Text(Translations.of(context).submitStepLabel),do
          state: StepState.indexed,
          content: _buildCustomerVerificationWidget(),
          isActive: !_isStepActive),
    ];

    return paymentSteps;
  }

  String _validateIdentifier(String value) {
    if (value.isEmpty || value.length < 11 || value.length > 13) {
      setState(() => _isIdentifierRequired = true);
      return Translations.of(context).invalidInput;
    }
    return "";
  }

  String _validateAmount(String value) {
    if (value.isEmpty) {
      setState(() => _isAmountRequired = true);
      return Translations.of(context).amountRequiredError;
    } else if (!RegexHelpers.amountValidatorRegex.hasMatch(value)) {
      return Translations.of(context).validAmountError;
    }
    return "";
  }

  String _validateOTP(String value) {
    if (value.isEmpty || value.length < 4) {
      setState(() => _isOTPRequired = true);
      return Translations.of(context).invalidOtp;
    }
    return "";
  }

  bool _validateInfoForm() {
    _isOTPRequired = false;
    _isAmountRequired = false;
    _isIdentifierRequired = false;

    if (!this._formKey.currentState.validate()) {
      return false;
    }
    _formKey.currentState.save();
    return true;
  }

  bool _validateOtpForm() {
    _isOTPRequired = false;
    _isAmountRequired = false;
    _isIdentifierRequired = false;

    if (!this._formKeyOTP.currentState.validate()) {
      return false;
    }
    _formKeyOTP.currentState.save();
    return true;
  }

  Future<bool> _sendOTP() async {
    bool sendOTP = false;
    setState(() {
      _isIdentifierRequired = false;
      _isAmountRequired = false;
      _isOTPRequired = false;
    });

    if (!_validateInfoForm()) {
      setState(() {
        _validateAmount(_controllerAmount.text);
        _validateIdentifier(_controllerIdentifier.text);
      });

      if (_validateAmount(_controllerAmount.text).isNotEmpty ||
          _validateIdentifier(_controllerIdentifier.text).isNotEmpty) {
        return false;
      }
    }
    try {
      showDialog(
        barrierDismissible: false,
        context: context,
        builder: (context) => AlertDialog(
              content: ListTile(
                leading: CircularProgressIndicator(),
                title: Text(Translations.of(context).processingSendOtpDialog),
              ),
            ),
      );

      TransactionApi api =
          new TransactionApi(httpDataSource, authenticator.sessionToken);
      sendOTP = await api.sendOTP(_controllerIdentifier.text);

      if (sendOTP) {
        _isStepActive = false;
        _controllerOTP.clear();
      }

      Navigator.of(context).pop();
    } catch (exception) {
      await showAlertDialog(context, Translations.of(context).pullPayment,
          '${exception.message}');
      Navigator.of(context).pop();
      return false;
    }

    return sendOTP;
  }

  Future<bool> _pullPayment() async {
    Result pullPaymentResponse = new Result();
    setState(() {
      _isIdentifierRequired = false;
      _isAmountRequired = false;
      _isOTPRequired = false;
    });

    if (!_validateOtpForm()) {
      setState(() {
        _validateOTP(_controllerAmount.text);
      });

      if (_validateAmount(_controllerAmount.text).isNotEmpty ||
          _validateIdentifier(_controllerIdentifier.text).isNotEmpty ||
          _validateOTP(_controllerOTP.text).isNotEmpty) {
        return false;
      }
    }

    try {
      setState(() {
        _isOTPRequired = false;
      });
      showDialog(
        barrierDismissible: false,
        context: context,
        builder: (context) => AlertDialog(
              content: ListTile(
                leading: CircularProgressIndicator(),
                title: Text(Translations.of(context).processingPaymentDialog),
              ),
            ),
      );

      TransactionApi api =
          new TransactionApi(httpDataSource, authenticator.sessionToken);
      pullPaymentResponse = await api.pullPayment(_controllerIdentifier.text,
          _controllerAmount.text, _controllerOTP.text);

      Navigator.of(context).pop();
    } catch (exception) {
      await showAlertDialog(context, Translations.of(context).pullPayment,
          '${exception.message}');
      Navigator.of(context).pop();
      return false;
    }

    if (successResponseCodes.contains(pullPaymentResponse.responseCode)) {
      await showAlertDialog(context, Translations.of(context).pullPayment,
          '${pullPaymentResponse.description}');
      Navigator.pop(context);
      return true;
    }
    return false;
  }
}
Sana.91
  • 1,999
  • 4
  • 33
  • 52
  • form won't validate until you tell it. Set `autoValidate` to true for each form whenever you want to validate or make it true by default. – ap14 Jul 15 '18 at 12:15
  • It validates whenever if (!this._formKey.currentState.validate()) { return false; } is called ... The issue is .. Its returning false even when input data is correct @ap14 – Sana.91 Jul 15 '18 at 15:50
  • i encountered a similar problem but my code works when I only write `_validateIdentifier` in validator property. Don't change the function just write only the function name `validator: _validateIdentifier` – ap14 Jul 16 '18 at 04:47
  • @ap14 i did not get you ... what are u asking to change – Sana.91 Jul 16 '18 at 12:52
  • Possible duplicate of [flutter stepper widget - validating fields in individual steps](https://stackoverflow.com/questions/51231128/flutter-stepper-widget-validating-fields-in-individual-steps) – Sana.91 Aug 29 '18 at 13:11

1 Answers1

-1

Firstly declare a bool variable.

 bool _autoValidate = false;

Then use this bool in your form's autoValidate property. As mentioned below

Form(
    key: _formKey,
    autovalidate: _autoValidate,
    child: Column(
      children: <Widget>[
        CustomTextField(
          focusNode: _focusNodeID,
          hintText: Translations.of(context).cnic,
          labelText: Translations.of(context).cnic,
          controller: _controllerIdentifier,
          keyboardType: TextInputType.number,
          hasError: _isIdentifierRequired,
          validator:  _validateIdentifier,
          maxLength: 13,
        ),
        CustomTextField(
          focusNode: _focusNodeAmount,
          hintText: Translations.of(context).amount,
          labelText: Translations.of(context).amount,
          controller: _controllerAmount,
          keyboardType: TextInputType.number,
          hasError: _isAmountRequired,
          validator: (String t) => _validateAmount(t),
          maxLength: 6,
        ),
      ],
    ));

Now whenever you want to validate the fields call this function.

setState(() {
      _autoValidate = true;
    });

This will automatically verify all the fields in the form which have a validator function.

And your validator functions goes like this.

String _validateIdentifier(String value) {
if (value.isEmpty || value.length < 11 || value.length > 13) {
  setState(() => _isIdentifierRequired = true);
  return Translations.of(context).invalidInput;
}
return null;

}

ap14
  • 4,393
  • 1
  • 15
  • 30
  • no this too return form state as invalid even though it is valid – Sana.91 Jul 23 '18 at 07:33
  • Firstly Use `TextFormField` as child of your `Form` then customize it on proper functioning. – ap14 Jul 23 '18 at 08:01
  • 1
    We just need to return null and not "" from validators, please check my answer – Sana.91 Aug 28 '18 at 20:08
  • @Sana.91 I have edited my answer. For a string `""` and `null` will result in no error message. Though use of `null` is a better approach. – ap14 Aug 29 '18 at 11:59
  • empty string "" will cause FormState.Validate to fail , please check docs https://flutter.io/cookbook/forms/validation/ – Sana.91 Aug 29 '18 at 12:09
  • @Sana.91 thanks for updating me on that. I hope you haven't waste time due to my error. – ap14 Aug 29 '18 at 12:20