Conversation
Two validate() calls in one synchronous turn shared one run: SharedCall cleared its in-flight future in a .then callback, a microtask later, even when the field had no async validator. The second call got a verdict computed before a dependency changed — exactly what the skill's "prefilled field must react" listener pattern runs into after an eager validate() in a form constructor. Drop SharedCall from both controllers. runValidate already shares what is worth sharing: an in-flight async round is awaited, a settled verdict reused. A sync error found by a later call aborts the round, so its answer cannot land over the error. The form's validate() calls every field's validate(), so a double-tapped submit still makes one set of async calls. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Albert221
requested review from
KamilSztandur,
PiotrRogulski and
mateusz-pietras
as code owners
September 23, 2026 15:04
|
Docs preview: https://advanced-forms-fjnmf8qj0-leancode.vercel.app Built from a53446c; the landing page is at |
Albert221
marked this pull request as draft
September 23, 2026 15:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
If you call
validate()twice in the same synchronous turn, the second call gets a stale answer when something the validator reads changed in between. Only the field's own value invalidates the shared run, so a change to a field it depends on is missed.0.2.2's skill ("When a prefilled field MUST react anyway") recommends calling
dependent.validate()from a listener on the watched field. Put that together with an eagervalidate()in the form constructor and it silently returns the old verdict. We ran into this while moving two production forms, including an aircraft booking form, to 0.2.2 and following that advice.Root cause
Both controllers route
validate()throughSharedCall, which clears its in-flight future in a.thencallback. That happens a microtask later, even when the validator finished synchronously and there is no async validator. Until then, every call joins the first run, and its verdict was computed before the dependency changed.Fix
SharedCallis gone andvalidate()callsrunValidatedirectly on both controllers:validate()returns.runValidatealready did this: an in-flight round is awaited rather than restarted, a debouncing round is flushed, and a settled verdict for an unchanged value is reused.asyncValidation, calls don't share anything. Each call is independent.false. This is the existing sync-failure path inrunValidate; I only added a comment.Nothing else changes: debounce, abort and verdict handling, a disposed field completing
false, and a superseded round reportingfalse.AdvancedFormController.validate()had the same bug. It shared through its ownSharedCall, so callingform.validate()twice in one turn after a value change returned the first result. I removed thatSharedCalltoo, which also removes theinvalidate()that ran when the mode changed.form.validate()calls every field'svalidate(), and those now share their async rounds. So a double-tapped submit still makes one set of async calls; the existing "concurrent calls share one async round" test still checks there is exactly one validator call.SharedCallhad no other users, so I deleted it along with its unit test.Behaviour changes to note
Futureobject. They still resolve to the same result and share one async round. Two tests assertedidentical(first, second), so I dropped that assertion and kept the one-validator-call check.validate()is in flight now returnsfalsefrom later calls instead of that in-flight future. The in-flight future resolvesfalseanyway, because disposing aborts the fields' rounds. The test is now "a disposed form with a call in flight completes false".Tests
New tests:
field_dependencies_test.dart, "validate on an untouched dependent":awaitbetween the calls;validate()in the constructor.field_validate_test.dart:form_controller_test.dart: a secondform.validate()in the same turn sees a value that changed in between.All five bug-case tests fail against 0.2.2 and pass with this change. The existing tests for sharing an async round, debounce flush, verdict reuse and disposal still pass.
flutter analyzeis clean,dart formathas nothing to change, and all 283 tests influtter testpass.Docs
validate()dartdoc on both controllers.docs/validation/validate.mdx,docs/internals/validate-and-failures.mdxanddocs/faq.mdx.## Unreleased.Unrelated:
example/test/screens/step_form_test.darthas one test, "turning the switch back off clears the invoice errors", that already fails onmain. It fails the same way with this branch, and CI doesn't run the example tests.🤖 Generated with Claude Code