Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FormBuilderState.isValid is false even though the form is valid #1157

Closed
erayerdin opened this issue Nov 28, 2022 · 12 comments · Fixed by #1232
Closed

FormBuilderState.isValid is false even though the form is valid #1157

erayerdin opened this issue Nov 28, 2022 · 12 comments · Fixed by #1232
Assignees
Labels
bug Something isn't working

Comments

@erayerdin
Copy link
Contributor

erayerdin commented Nov 28, 2022

Environment

Package version: 7.7.0

Flutter doctor
[✓] Flutter (Channel stable, 3.3.9, on Fedora Linux 36 (KDE Plasma) 6.0.9-200.fc36.x86_64, locale en_US.UTF-8)
    • Flutter version 3.3.9 on channel stable at /home/erayerdin/.local/lib/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision b8f7f1f986 (6 days ago), 2022-11-23 06:43:51 +0900
    • Engine revision 8f2221fbef
    • Dart version 2.18.5
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at /home/erayerdin/.sdks/android/
    • Platform android-33, build-tools 33.0.0
    • ANDROID_SDK_ROOT = /home/erayerdin/.sdks/android
    • Java binary at: /var/lib/snapd/snap/android-studio/125/android-studio/jre/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop
    • clang version 14.0.5 (Fedora 14.0.5-1.fc36)
    • cmake version 3.24.2
    • ninja version 1.10.2
    • pkg-config version 1.8.0

[✓] Android Studio (version 2021.3)
    • Android Studio at /var/lib/snapd/snap/android-studio/125/android-studio
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] VS Code
    • VS Code at /snap/code/current
    • Flutter extension version 3.52.0

[✓] Connected device (2 available)
    • Linux (desktop) • linux  • linux-x64      • Fedora Linux 36 (KDE Plasma) 6.0.9-200.fc36.x86_64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 107.0.5304.121

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
Code sample
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FormPage(),
    );
  }
}

class FormPage extends StatefulWidget {
  final _formKey = GlobalKey<FormBuilderState>();

  FormPage({super.key});

  @override
  State<FormPage> createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {
  bool? isValid;

  @override
  Widget build(BuildContext context) {
    return FormBuilder(
      key: widget._formKey,
      autovalidateMode: AutovalidateMode.always,
      onChanged: () {
        // rebuild when form changes
        isValid = widget._formKey.currentState?.isValid;
        setState(() {});
      },
      child: Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              // UI BELOW //
              children: [
                FormBuilderTextField(
                  name: 'email',
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                  ),
                  validator: FormBuilderValidators.compose([
                    FormBuilderValidators.required(),
                    FormBuilderValidators.email(),
                  ]),
                ),
                const SizedBox(height: 16),
                Text(
                  isValid == null
                      ? 'initial state'
                      : isValid!
                          ? 'valid'
                          : 'invalid',
                ),
                // UI ABOVE //
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Description

Expected behavior: _formKey.currentState.isValid should return true when all the fields in the form are valid.

Current behavior: It does not return true.

Steps to reproduce

  1. Launch the app.
  2. Fill a valid email.
  3. In password field, only put 8 characters. At this point the form is technically valid and there are no error texts.
  4. The value of isValid is false because _formKey.currentState.isValid is false.
  5. In password field, put one more character to make it 9 characters in total.
  6. Now, the form is valid.

Images

image

Stacktrace/Logcat

[        ] DwdsVmClient: Attempting a hot restart
[        ] DwdsVmClient: Attempting to disable breakpoints and resume the isolate
[   +1 ms] DwdsVmClient: Successfully disabled breakpoints and resumed the isolate
[        ] DwdsVmClient: Attempting to get execution context ID.
[        ] DwdsVmClient: Got execution context ID.
[        ] DwdsVmClient: Issuing $dartHotRestartDwds request
[ +361 ms] ChromeProxyService: Initializing expression compiler for main_module.bootstrap.js with sound null safety: true
[  +20 ms] DwdsVmClient: $dartHotRestartDwds request complete.
[        ] DwdsVmClient: Waiting for Isolate Start event.
[  +57 ms] DwdsVmClient: Successful hot restart
[   +8 ms] Restarted application in 571ms.
[  +31 ms] DevHandler: VmService proxy responded with an error:
           {jsonrpc: 2.0, id: 144, error: {code: -32601, message: Method not found, data: {jsonrpc: 2.0, method: _setStreamIncludePrivateMembers, id: 144, params: {streamId: Isolate, includePrivateMembers: false}}}}
[  +52 ms] [🌎 Easy Localization] [DEBUG] Localization initialized
[   +2 ms] DevHandler: VmService proxy responded with an error:
           {jsonrpc: 2.0, id: 149, error: {code: -32601, message: setLibraryDebuggable: Not supported on web devices}}
[   +3 ms] WARNING: You are using the Auth Emulator, which is intended for local testing only.  Do not use with production credentials.
[  +14 ms] [2022-11-28T12:33:35.782Z]  @firebase/firestore:
[   +2 ms] [🌎 Easy Localization] [DEBUG] Start
[  +64 ms] [🌎 Easy Localization] [DEBUG] Init state
[   +7 ms] [🌎 Easy Localization] [DEBUG] Build
[   +4 ms] [🌎 Easy Localization] [DEBUG] Init Localization Delegate
[   +1 ms] [🌎 Easy Localization] [DEBUG] Init provider
[  +58 ms] [🌎 Easy Localization] [DEBUG] Load Localization Delegate
[log] easy localization loader: load yaml file assets/translations/en-US.yaml
[ +352 ms] �[3;38;5;244m🐛 15:33:36.278 DEBUG    [Logic][LoggingBlocObserver] - Instance of 'FormCubit' is being created.
[   +1 ms] 🤔 15:33:36.280 TRACE    [Logic][LoggingBlocObserver] - bloc: Instance of 'FormCubit'
[+272865 ms] false
[   +3 ms] �[3;38;5;244m🐛 15:38:09.151 DEBUG    [Logic][LoggingBlocObserver] - Instance of 'FormCubit' is on change.
[   +2 ms] 🤔 15:38:09.154 TRACE    [Logic][LoggingBlocObserver] - change: Change { currentState: FormState.initial(), nextState: FormState.invalid(fields: {email: _FormBuilderTextFieldState#56f1d, password: _FormBuilderTextFieldState#cd37b}) }
[ +156 ms] false
[  +88 ms] false
[ +178 ms] false
[ +208 ms] false
[  +81 ms] false
[ +277 ms] false
[ +223 ms] false
[ +144 ms] false
[ +111 ms] false
[ +320 ms] false
[  +81 ms] false
[ +267 ms] ══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
[        ] The following assertion was thrown during a platform message callback:
[        ] Assertion failed:
[        ]
[        ] event is! RawKeyDownEvent || _keysPressed.isNotEmpty
[        ] "Attempted to send a key down event when no keys are in keysPressed. This state can occur if the key
[        ] event being sent doesn't properly set its modifier flags. This was the event:
[        ] RawKeyDownEvent#cbfbd(logicalKey: LogicalKeyboardKey#9d80f(keyId: \"0x17032c92bb\", keyLabel: \"\",
[        ] debugName: \"Key with ID 0x017032c92bb\"), physicalKey: PhysicalKeyboardKey#700e6(usbHidUsage:
[        ] \"0x000700e6\", debugName: \"Alt Right\"), repeat: false) and its data:
[        ] RawKeyEventDataWeb#6459c(code: AltRight, key: AltGraph, location: 2, metaState: 0, keyCode: 225)"
[        ] When the exception was thrown, this was the stack:
[        ] dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 266:49      throw_
[        ] dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 29:3        assertFailed
[        ] packages/flutter/src/services/raw_keyboard.dart 682:49                            handleRawKeyEvent
[        ] packages/flutter/src/services/hardware_keyboard.dart 878:30                       handleRawKeyMessage
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 84:54                runBody
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 123:5                _async
[        ] packages/flutter/src/services/hardware_keyboard.dart 850:51                       handleRawKeyMessage
[        ] packages/flutter/src/services/platform_channel.dart 197:49                        <fn>
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 84:54                runBody
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 123:5                _async
[        ] packages/flutter/src/services/platform_channel.dart 196:58                        <fn>
[        ] packages/flutter/src/services/binding.dart 387:35                                 <fn>
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 84:54                runBody
[        ] dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 123:5                _async
[        ] packages/flutter/src/services/binding.dart 384:98                                 <fn>
[        ] lib/_engine/engine/platform_dispatcher.dart 1199:13                               invoke2
[        ] lib/ui/channel_buffers.dart 25:12                                                 invoke
[        ] lib/ui/channel_buffers.dart 65:7                                                  push
[        ] lib/ui/channel_buffers.dart 130:16                                                push
[        ] lib/_engine/engine/platform_dispatcher.dart 364:25                                invokeOnPlatformMessage
[        ] lib/_engine/engine/keyboard.dart 130:39                                           [_handleHtmlEvent]
[        ] lib/_engine/engine/keyboard.dart 32:7                                             <fn>
[        ] dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
[        ] dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
[        ] ════════════════════════════════════════════════════════════════════════════════════════════════════
[   +2 ms] false
[ +188 ms] false
[ +136 ms] false
[  +92 ms] false
[ +139 ms] false
[  +86 ms] false
[ +208 ms] false
[ +129 ms] false
[ +124 ms] false
[  +88 ms] false
[ +867 ms] false
[ +174 ms] false
[ +167 ms] false
[ +152 ms] false
[ +295 ms] false
[ +167 ms] false
[ +168 ms] false
[ +168 ms] false
[+107543 ms] Another exception was thrown: Assertion failed:
[+53881 ms] true
[   +2 ms] �[3;38;5;244m🐛 15:40:56.085 DEBUG    [Logic][LoggingBlocObserver] - Instance of 'FormCubit' is on change.
[   +1 ms] 🤔 15:40:56.088 TRACE    [Logic][LoggingBlocObserver] - change: Change { currentState: FormState.invalid(fields: {email: _FormBuilderTextFieldState#56f1d, password: _FormBuilderTextFieldState#cd37b}), nextState: FormState.valid(fields: {email: _FormBuilderTextFieldState#56f1d, password: _FormBuilderTextFieldState#cd37b}) }
@erayerdin erayerdin added the bug Something isn't working label Nov 28, 2022
@deandreamatias
Copy link
Collaborator

deandreamatias commented Nov 28, 2022

In this cases the better way to avoid external errors, is build minimal example to reproduce the issue.
With this example, I can't reproduce the error because doesn't has the Bloc dependencies, like FormCubit,
Another thing that I migth need for test, is the password or some similar string

@erayerdin
Copy link
Contributor Author

Here's the simplest app that fails.

It's a web app.

Peek.2022-11-29.12-47.mp4

As you can see, even though I put [email protected] and it is valid since there are no error texts, the validity text does not change.

The validity text, however, changes when I put [email protected]. It seems like FormBuilderState.isValid is coming from a step behind.

@erayerdin
Copy link
Contributor Author

erayerdin commented Nov 29, 2022

The strange thing is, a plain function validator works as I expected, see this:

FormBuilderTextField(
  name: 'email',
  decoration: const InputDecoration(
    labelText: '[email protected]',
    border: OutlineInputBorder(),
  ),
  // instead of FormBuilderValidators.email, a plain function is here
  validator: (value) {
    // if the input is empty
    if (value == null) {
      return 'Field is required';
    }

    // if the input is not empty

    // store these conditions in a list of bools
    final conditions = [
      value.contains('@'), // input has `@`
      value.contains('.'), // input has a dot
      !value.startsWith('@'), // input does not start with `@`
      !value.startsWith('.'), // input does not start with a dot
      !value.endsWith('@'), // input does not end with `@`
      !value.endsWith('.'), // input does not end with a dot
    ];

    // if any one of these conditions are false
    if (!conditions.every((element) => element)) {
      return 'Email is invalid.';
    }

    // if all conditions are true
    return null;
  },
),

So, this might be a bug in form_builder_validators?

@erayerdin
Copy link
Contributor Author

Not only FormBuilderValidators.email, but FormBuilderValidators.minLength fails to this as well.

@erayerdin
Copy link
Contributor Author

erayerdin commented Nov 29, 2022

I confirmed this email regex pattern has no problem. Also considering FormBuilderValidators.minLength fails, I think this is not a problem originating from form_builder_validators.

Also, FormBuilder.onChanged has no problem because it works with plain function validators. So, it also has no probsies with it.

This rabbit hole goes deeper baby.

For now, I will stick to plain function validators in my app until we can figure this out.

@deandreamatias
Copy link
Collaborator

Should try run minLength validators tests with your password.
I'm not sure where is your problem, but try apply your validator on example app, only with minimal widgets, no bloc or other things

@erayerdin
Copy link
Contributor Author

Sure thing, I can even provide the code directly. This uses StatefulWidget instead of Bloc.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FormPage(),
    );
  }
}

class FormPage extends StatefulWidget {
  final _formKey = GlobalKey<FormBuilderState>();

  FormPage({super.key});

  @override
  State<FormPage> createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {
  bool? isValid;

  @override
  Widget build(BuildContext context) {
    return FormBuilder(
      key: widget._formKey,
      autovalidateMode: AutovalidateMode.always,
      onChanged: () {
        // rebuild when form changes
        isValid = widget._formKey.currentState?.isValid;
        setState(() {});
      },
      child: Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              // UI BELOW //
              children: [
                FormBuilderTextField(
                  name: 'email',
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                  ),
                  validator: FormBuilderValidators.compose([
                    FormBuilderValidators.required(),
                    FormBuilderValidators.email(),
                  ]),
                ),
                const SizedBox(height: 16),
                Text(
                  isValid == null
                      ? 'initial state'
                      : isValid!
                          ? 'valid'
                          : 'invalid',
                ),
                // UI ABOVE //
              ],
            ),
          ),
        ),
      ),
    );
  }
}

If you input [email protected] as input, the value for _FormPageState.isValid will be false even though there's clearly no error or error text in the field.

However, the second you put a new character, let's say [email protected], _FormPageState.isValid will be true.

The value of _FormPageState.isValid comes from FormBuilder.onChanged, which gets the value from FormBuilderState.isValid.

@deandreamatias
Copy link
Collaborator

deandreamatias commented Dec 5, 2022

We need solve if this issue is from validator or from logic of isValid property. After that, can investigate what is the issue with this internal code

@icelija
Copy link

icelija commented Jan 4, 2023

Hello, @erayerdin I encountered the same issue in my example and the only workaround I found for this issue is that you should wrap _formKey.currentState?.isValid call within WidgetsBinding.instance.addPostFrameCallback((_) {}). Then everything works as expected and you get isValid true immediately after the field becomes valid.

So the onChanged callback in your FormBuilder should look like this:

     onChanged: () {
        // rebuild when form changes
        WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
          isValid = widget._formKey.currentState?.isValid;
          setState(() {});
        });
      },

@erayerdin
Copy link
Contributor Author

You're a lifesaver @icelija. Unfortunately, I'm not dealing with this thing right now, let's wait for someone else to validate it works for them.

Other than that, the main problem I think is flutter_form_builder using the GlobalKey. It initializes currentState when the widget is mounted. So currentState is not initialized when GlobalKey initialized.

I can get why it is the way it is though. When you develop a third-party package, it's better to depend on the standard library rather than another third-party one. The way it works also has caused me headaches in #1162.

@deandreamatias
Copy link
Collaborator

I'm working on this bug on branch improve-autovalidate-mode

@deandreamatias
Copy link
Collaborator

Can test this on pre-release 9.0.0-dev.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants