Sana.91
Sana.91

Reputation: 2249

flutter stepper widget - validating fields in individual steps

i am using stepper widget in order to collect info from user and validate it, i need to call an API at each step hence validate each field in a step at every continue button ... i am using form state and form widget but the issue is that it validates entire fields in all steps in stepper... how can i validate only individual step in a stepper? i went through the documentation in Stepper and State classes in stepper.dart but there is no supporting function there

following is the code

class SubmitPayment extends StatefulWidget {


 SubmitPayment({Key key, this.identifier, this.amount, this.onResendPressed})
      : super(key: key);

  final String identifier;
  final String amount;
  final VoidCallback onResendPressed;

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

class _SubmitPaymentState extends State<SubmitPayment> {
  final GlobalKey<FormState> _formKeyOtp = GlobalKey<FormState>();
  final FocusNode _otpFocusNode = FocusNode();
  final TextEditingController _otpController = TextEditingController();
  bool _isOTPRequired = false;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8.0),
      child: 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: TextStyle(
                          color: Theme.of(context).primaryColor,
                          fontStyle: FontStyle.italic),
                    ))),
            CustomTextField(
              icon: Icons.vpn_key,
              focusNode: _otpFocusNode,
              hintText: Translations.of(context).otp,
              labelText: Translations.of(context).otp,
              controller: _otpController,
              keyboardType: TextInputType.number,
              hasError: _isOTPRequired,
              validator: (String t) => _validateOTP(t),
              maxLength: AppConstants.otpLength,
              obscureText: true,
            ),
            Center(
                child: ButtonBar(
              mainAxisSize: MainAxisSize.max,
              alignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  child: Text(Translations.of(context).resendOtpButton),
                  color: Colors.white,
                  textColor: Theme.of(context).primaryColor,
                  onPressed: widget.onResendPressed,
                ),
                RaisedButton(
                  child: Text(
                    Translations.of(context).payButton,
                  ),
                  onPressed: _doPullPayment,
                ),
              ],
            )),
          ])),
    );
  }

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

  bool _validateOtpForm() {
    _formKeyOtp.currentState.save();
    return this._formKeyOtp.currentState.validate();
  }

  Future<void> _doPullPayment() async {
    setState(() {
      _isOTPRequired = false;
    });

    if (!_validateOtpForm()) 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 =
          TransactionApi(httpDataSource, authenticator.sessionToken);
      String responseMessage = await api.doPullPayment(
          widget.identifier,
          widget.amount,
          _otpController.text,
          TransactionConstants.transactionCurrency);

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

Upvotes: 5

Views: 10285

Answers (3)

Derek Lakin
Derek Lakin

Reputation: 16319

One approach is to use a separate Form for each step. To handle that, use a list of GlobalKey<FormState> which you can index based on _currentStep, then call validate() in onStepContinue:

List<GlobalKey<FormState>> _formKeys = [GlobalKey<FormState>(), GlobalKey<FormState>(), …];
…

Stepper(
  currentStep: _currentStep,
  onStepContinue: () {
    setState(() {
      if (_formKeys[_currentStep].currentState?.validate()) {
        _currentStep++;
      }
    });
  },
  steps: 
  Step(
    child: Form(key: _formKeys[0], child: …),

This implies the following:

  1. Since you're calling an API at the end, you need to check if you're validating the last step, and save instead of just validating;
  2. You probably want to factor our the Forms to several widgets. If you do so, do not confuse the key parameter that every Widget has. Pass the formKey as an unnamed parameter to avoid confusion.

Upvotes: 14

Siewe Rostand
Siewe Rostand

Reputation: 41

It's been long since this question was asked. I hope my answer can help. To do this, I created a List<GlobalKey> then in the onContinue of the Stepper I did something as

final List<GlobalKey<FormState>> _formKeys = [
    GlobalKey<FormState>(),
    GlobalKey<FormState>(),
    GlobalKey<FormState>(),
    GlobalKey<FormState>()
  ];                                                                 continued()  {
    if(_formKeys[_currentStep].currentState!.validate()) {
      switch(_currentStep){
      case 0:
        setSender();
        break;
      case 1:
        setReceiver();
        break;
    }
    }

}

Upvotes: 1

Sana.91
Sana.91

Reputation: 2249

So i solved this as follows:

The problem was that i was returning an *empty string ("") * if the my logic was valid, where as validate method of FormState expects each validator method, associated with TextFormField to return null if validation is passed.

i changed following

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

to

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

}

and it worked all fine then.

Refer to this link for details "If there is an error with the information the user has provided, the validator function must return a String containing an error message. If there are no errors, the function should not return anything."

Upvotes: 1

Related Questions