From fc53c713815301ba455a7909c539180889a382c6 Mon Sep 17 00:00:00 2001 From: Mairramer <50643541+Mairramer@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:11:00 -0300 Subject: [PATCH] Fixes initial validation with AutovalidateMode.always on first build (#156708) Fixes #142701 This PR fixes an issue where on the first build `AutovalidateMode.always` was not called. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter/lib/src/widgets/form.dart | 33 +++++++++------ .../test/material/time_picker_test.dart | 2 - .../test/material/time_picker_theme_test.dart | 2 - packages/flutter/test/widgets/form_test.dart | 40 +++++++++++++++++++ 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 7be29d6eaa..8fe5cf786b 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'binding.dart'; import 'focus_manager.dart'; import 'focus_scope.dart'; import 'framework.dart'; @@ -250,22 +251,10 @@ class FormState extends State
{ void _register(FormFieldState field) { _fields.add(field); - if (widget.autovalidateMode == AutovalidateMode.onUnfocus) { - field._focusNode.addListener(() => _updateField(field)); - } } void _unregister(FormFieldState field) { _fields.remove(field); - if (widget.autovalidateMode == AutovalidateMode.onUnfocus) { - field._focusNode.removeListener(()=> _updateField(field)); - } - } - - void _updateField(FormFieldState field) { - if (!field._focusNode.hasFocus) { - _validate(); - } } @protected @@ -703,6 +692,25 @@ class FormFieldState extends State> with RestorationMixin { } @protected + @override + void didChangeDependencies() { + super.didChangeDependencies(); + switch (Form.maybeOf(context)?.widget.autovalidateMode) { + case AutovalidateMode.always: + WidgetsBinding.instance.addPostFrameCallback((_) { + // If the form is already validated, don't validate again. + if (widget.enabled && !hasError && !isValid) { + validate(); + } + }); + case AutovalidateMode.onUnfocus: + case AutovalidateMode.onUserInteraction: + case AutovalidateMode.disabled: + case null: + break; + } + } + @override void dispose() { _errorText.dispose(); @@ -749,7 +757,6 @@ class FormFieldState extends State> with RestorationMixin { return widget.builder(this); } - } /// Used to configure the auto validation of [FormField] and [Form] widgets. diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 07b4339059..bbdbacc6ac 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -2074,7 +2074,6 @@ void main() { // Enter invalid hour. await tester.enterText(find.byType(TextField).first, 'AB'); await tester.tap(find.text(okString)); - await tester.pumpAndSettle(); expect(tester.getSize(findBorderPainter().first), const Size(96.0, 70.0)); }); @@ -2093,7 +2092,6 @@ void main() { // Enter invalid hour. await tester.enterText(find.byType(TextField).first, 'AB'); await tester.tap(find.text(okString)); - await tester.pumpAndSettle(); expect(tester.getSize(findBorderPainter().first), const Size(96.0, 70.0)); }); diff --git a/packages/flutter/test/material/time_picker_theme_test.dart b/packages/flutter/test/material/time_picker_theme_test.dart index 3251ddfbf4..3c7207380a 100644 --- a/packages/flutter/test/material/time_picker_theme_test.dart +++ b/packages/flutter/test/material/time_picker_theme_test.dart @@ -872,7 +872,6 @@ void main() { // Enter invalid hour. await tester.enterText(find.byType(TextField).first, 'AB'); await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); expect(tester.getSize(findBorderPainter().first), const Size(96.0, 72.0)); }); @@ -890,7 +889,6 @@ void main() { // Enter invalid hour. await tester.enterText(find.byType(TextField).first, 'AB'); await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); expect(tester.getSize(findBorderPainter().first), const Size(96.0, 70.0)); }); diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index 9785d7c28d..5a3117d47d 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -1457,4 +1457,44 @@ void main() { expect(focusNode2.hasFocus, isTrue); }); + + testWidgets('AutovalidateMode.always should validate on second build', (WidgetTester tester) async { + String errorText(String? value) => '$value/error'; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(), + home: Center( + child: Form( + autovalidateMode: AutovalidateMode.always, + child: Material( + child: Column( + children: [ + TextFormField( + initialValue: 'foo', + validator: errorText, + ), + TextFormField( + initialValue: 'bar', + validator: errorText, + ), + ], + ), + ), + ), + ), + ), + ); + + // The validation happens in a post frame callback, so the error + // doesn't show up until the second frame. + expect(find.text(errorText('foo')), findsNothing); + expect(find.text(errorText('bar')), findsNothing); + + await tester.pump(); + + // The error shows up on the second frame. + expect(find.text(errorText('foo')), findsOneWidget); + expect(find.text(errorText('bar')), findsOneWidget); + }); }