Flutter formKey currentstate is null when pass to another widget
Asked Answered
W

4

6

I'm trying to create a Textbutton widget with a disabled property like this:

class AppTextButton extends StatelessWidget {
  final String title;
  final void Function(BuildContext context) onPress;
  final EdgeInsetsGeometry margin;
  final EdgeInsetsGeometry padding;
  final double borderRadius;
  final Color backgroundColor;
  final Image? leadingIcon;
  final Image? trailingIcon;
  final TextStyle? textStyle;
  final bool disabled;

  AppTextButton(this.title, this.onPress,
      {this.margin = const EdgeInsets.all(0),
      this.padding = const EdgeInsets.all(12),
      this.borderRadius = 0,
      this.leadingIcon,
      this.trailingIcon,
      this.textStyle,
      this.disabled = false,
      this.backgroundColor = const Color(0xFFFFFFFF)});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: margin,
      child: TextButton(
        style: ButtonStyle(
            shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(borderRadius))),
            backgroundColor: MaterialStateProperty.all(backgroundColor)),
        child: Row(
          children: [
            if (this.leadingIcon != null) ...[this.leadingIcon!],
            Expanded(
              child: Padding(
                padding: padding,
                child:
                    Text(title, textAlign: TextAlign.center, style: textStyle),
              ),
            ),
            if (this.trailingIcon != null) ...[this.trailingIcon!]
          ],
        ),
        onPressed: () => !disabled ? onPress(context) : null,
      ),
    );
  }
}

And in my screen, I declare my formKey and my form as following:

class LoginScreen extends AppBaseScreen {
  LoginScreen({Key? key}) : super(key: key);

  final _formKey = GlobalKey<FormState>();

@override
  Widget build(BuildContext context) {
              Form(
                  key: _formKey,
                  child: Obx(
                    () => AppTextInput(
                      "Please input passcode",
                      _passwordController,
                      borderRadius: 8,
                      fillColor: Color(0xFFF6F4F5),
                      keyboardType: TextInputType.number,
                      errorMessage: _c.errorLoginConfirm.value,
                      isObscure: true,
                      onChange: _onInputChange,
                      maxLength: 6,
                      margin: EdgeInsets.only(top: 12, left: 20, right: 20),
                      validator: (text) {
                        if (text != null && text.length > 0) {
                          if (text.length < 6) {
                            return "Passcode must have at least 6 digits";
                          }
                        }
                      },
                    ),
                  )),

And I will have a button at the bottom of the screen, which I pass the !_formKey.currentState!.validate() in the disabled field

AppTextButton("Login", _onLogin,
                  margin: EdgeInsets.fromLTRB(24, 24, 24, 8),
                  backgroundColor: Color(0xFFFF353C),
                  disabled: !_formKey.currentState!.validate(),
                  textStyle: TextStyle(color: Colors.white),
                  borderRadius: 8),

However, the formKey.currentState is null and throw the following error everytime the screen is opened. Null check operator used on a null value

What I am doing wrong here? Thank you in advance!

Warnock answered 31/8, 2021 at 8:12 Comment(2)
Have you tried using a stateful widget? Since your UI needs to be updated depending on form validation state, it is not a stateless widget.Zoezoeller
Hi @PeterKoltai I use separate controller and binding from https://pub.dev/packages/getWarnock
I
2

You need to save the form state before passing,

final FormState formState = _formKey.currentState;
formState.save();


onPressed: () {
                FocusScope.of(context).requestFocus(FocusNode());
                final FormState formState = _formKey.currentState;
                if (formState.validate()) {
                  formState.save();
                  onPress(context);
                }
              },
Indoctrinate answered 4/9, 2021 at 3:5 Comment(1)
Hi Sachin, where should I put the above two lines? Currently, I put them inside build function. However, when I tried to print the _formKey.currentState, the result is null.Warnock
I
1

In your case, you should know how the widgets building process (Assume you have Botton widget and Input widget):

  1. Botton and Input are building initial state. both states are not yet ready to be read and used
  2. Botton and Input are built. States are ready to read.
  3. User interact to Input. Input must call Button to rebuild its state if the value passes the validator
  4. Botton rebuild.

For the process, you should change your code like:

  1. Get and modify the state of Button inside Input
  2. Notify Button to rebuild

There are many ways to handle the state management between widgets. I simply change the AppTextButton into Statefultwidget to achieve it.

...
final _buttonKey = GlobalKey<_AppTextButtonState>();
...
  AppTextButton(key: _buttonKey)
...


class AppTextButton extends StatefulWidget {
  final bool initDisable;

  AppTextButton({
    this.initDisable = false,
    Key? key,
  }) : super(key: key);

  @override
  _AppTextButtonState createState() => _AppTextButtonState();
}

class _AppTextButtonState extends State<AppTextButton> {
  var disable;

  @override
  void initState() {
    disable = widget.initDisable;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(child: Text('Button'), onPressed: disable ? null : () {});
  }

  void enableButton() {
    setState(() {
      disable = false;
    });
  }

  void disableButton() {
    setState(() {
      disable = true;
    });
  }
}


class LoginScreen extends StatelessWidget {
  LoginScreen({Key? key}) : super(key: key);

  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: TextFormField(
        autovalidateMode: AutovalidateMode.onUserInteraction,
        validator: (text) {
          if (text != null && text.length > 0) {
            if (text.length < 6) {
              return "Passcode must have at least 6 digits";
            }
          }
        },
        onChanged: (v) {
          if (_formKey.currentState?.validate() ?? false) {
            _buttonKey.currentState?.enableButton();
          } else {
            _buttonKey.currentState?.disableButton();
          }
        },
      ),
    );
  }
}
Invariant answered 10/9, 2021 at 1:53 Comment(0)
H
0

I think the problem is caused because all the widgets are created at the same time, so the _formKey.currentState is still null when the AppTextButton calls it.

You need to create a separate controller to control the state of the button and add it to the validator like this:

validator: (text) {

                     if (text != null && text.length > 0) {
                        if (text.length < 6) {
                           buttonDisableController = true;
                           return "Passcode must have at least 6 digits";
                        }
                     }
                     buttonDisableController = false;
                     return null;
                  },
Henton answered 4/9, 2021 at 7:5 Comment(0)
G
0

You probably don't need this anymore but for those who came across and needs a way to handle:

Form state is null on page build. Hence you can't give it directly. You need to use onChange callback of text input widget.

bool disabled = true;
...
onChange: (text) => setState(() => disabled = !_formKey.currentState!.validate())
...
disabled: disabled,

and give disabled value to AppTextButton

note: this will do auto validation on each change in inputs and will show validation messages

note: you can handle state management in the way you wish

Gill answered 12/10, 2023 at 6:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.