4

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!

SuperStormer
  • 4,997
  • 5
  • 25
  • 35
hrtlkr29
  • 383
  • 1
  • 7
  • 21
  • 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. – Peter Koltai Aug 31 '21 at 08:59
  • Hi @PeterKoltai I use separate controller and binding from `https://pub.dev/packages/get` – hrtlkr29 Aug 31 '21 at 10:17

3 Answers3

1

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);
                }
              },
Sachin Liyanage
  • 534
  • 4
  • 15
  • 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. – hrtlkr29 Sep 04 '21 at 04:21
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;
                  },
Lam Thanh Nhan
  • 486
  • 4
  • 5
0

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();
          }
        },
      ),
    );
  }
}
yellowgray
  • 4,006
  • 6
  • 28