Fix crash when errorBuilder is used with decoration containing errorText#183384
Fix crash when errorBuilder is used with decoration containing errorText#18338404cb wants to merge 1 commit into
Conversation
Fixes #183378. When using errorBuilder alongside a decoration that has errorText, both error and errorText end up non-null, triggering the assertion "Declaring both error and errorText is not supported." This happens because InputDecoration.copyWith() cannot clear fields (passing null means "keep existing value"). The fix explicitly creates a new InputDecoration with errorText set to null when errorBuilder is used, ensuring only one error display mechanism is active at a time.
There was a problem hiding this comment.
Code Review
This pull request correctly fixes a crash that occurs when errorBuilder is used with a decoration that also defines errorText. The approach of creating a new InputDecoration instance to explicitly nullify errorText is sound. However, this has been implemented in a very verbose way in both DropdownButtonFormField and TextFormField, leading to significant code duplication. My review includes suggestions to refactor this logic to improve maintainability.
Note: Security Review is unavailable for this PR.
| if (error != null) { | ||
| // When using errorBuilder, we need to explicitly clear errorText since | ||
| // copyWith cannot clear fields (null means "keep existing value"). | ||
| effectiveDecoration = InputDecoration( | ||
| error: error, | ||
| errorText: null, | ||
| hintText: hintText, | ||
| icon: effectiveDecoration.icon, | ||
| iconColor: effectiveDecoration.iconColor, | ||
| label: effectiveDecoration.label, | ||
| labelText: effectiveDecoration.labelText, | ||
| labelStyle: effectiveDecoration.labelStyle, | ||
| floatingLabelStyle: effectiveDecoration.floatingLabelStyle, | ||
| helper: effectiveDecoration.helper, | ||
| helperText: effectiveDecoration.helperText, | ||
| helperStyle: effectiveDecoration.helperStyle, | ||
| helperMaxLines: effectiveDecoration.helperMaxLines, | ||
| hint: effectiveDecoration.hint, | ||
| hintStyle: effectiveDecoration.hintStyle, | ||
| hintTextDirection: effectiveDecoration.hintTextDirection, | ||
| hintFadeDuration: effectiveDecoration.hintFadeDuration, | ||
| hintMaxLines: effectiveDecoration.hintMaxLines, | ||
| maintainHintHeight: effectiveDecoration.maintainHintHeight, | ||
| maintainHintSize: effectiveDecoration.maintainHintSize, | ||
| maintainLabelSize: effectiveDecoration.maintainLabelSize, | ||
| errorStyle: effectiveDecoration.errorStyle, | ||
| errorMaxLines: effectiveDecoration.errorMaxLines, | ||
| floatingLabelBehavior: effectiveDecoration.floatingLabelBehavior, | ||
| floatingLabelAlignment: effectiveDecoration.floatingLabelAlignment, | ||
| isCollapsed: effectiveDecoration.isCollapsed, | ||
| isDense: effectiveDecoration.isDense, | ||
| contentPadding: effectiveDecoration.contentPadding, | ||
| prefixIcon: effectiveDecoration.prefixIcon, | ||
| prefix: effectiveDecoration.prefix, | ||
| prefixText: effectiveDecoration.prefixText, | ||
| prefixIconConstraints: effectiveDecoration.prefixIconConstraints, | ||
| prefixStyle: effectiveDecoration.prefixStyle, | ||
| prefixIconColor: effectiveDecoration.prefixIconColor, | ||
| suffixIcon: effectiveDecoration.suffixIcon, | ||
| suffix: effectiveDecoration.suffix, | ||
| suffixText: effectiveDecoration.suffixText, | ||
| suffixStyle: effectiveDecoration.suffixStyle, | ||
| suffixIconColor: effectiveDecoration.suffixIconColor, | ||
| suffixIconConstraints: effectiveDecoration.suffixIconConstraints, | ||
| counter: effectiveDecoration.counter, | ||
| counterText: effectiveDecoration.counterText, | ||
| counterStyle: effectiveDecoration.counterStyle, | ||
| filled: effectiveDecoration.filled, | ||
| fillColor: effectiveDecoration.fillColor, | ||
| focusColor: effectiveDecoration.focusColor, | ||
| hoverColor: effectiveDecoration.hoverColor, | ||
| errorBorder: effectiveDecoration.errorBorder, | ||
| focusedBorder: effectiveDecoration.focusedBorder, | ||
| focusedErrorBorder: effectiveDecoration.focusedErrorBorder, | ||
| disabledBorder: effectiveDecoration.disabledBorder, | ||
| enabledBorder: effectiveDecoration.enabledBorder, | ||
| border: effectiveDecoration.border, | ||
| enabled: effectiveDecoration.enabled, | ||
| semanticCounterText: effectiveDecoration.semanticCounterText, | ||
| alignLabelWithHint: effectiveDecoration.alignLabelWithHint, | ||
| constraints: effectiveDecoration.constraints, | ||
| visualDensity: effectiveDecoration.visualDensity, | ||
| ); | ||
| } else { | ||
| effectiveDecoration = effectiveDecoration.copyWith( | ||
| errorText: errorText, | ||
| hintText: hintText, | ||
| ); | ||
| } |
There was a problem hiding this comment.
While creating a new InputDecoration instance is the correct way to solve this issue (given that copyWith cannot nullify fields), this implementation is very verbose and hard to maintain. A nearly identical block of code has been added in text_form_field.dart, which introduces duplication.
To improve maintainability, consider extracting this logic into a shared helper function to avoid repeating this large constructor call.
| ? InputDecoration( | ||
| error: errorBuilder(state.context, errorText), | ||
| errorText: null, | ||
| icon: effectiveDecoration.icon, | ||
| iconColor: effectiveDecoration.iconColor, | ||
| label: effectiveDecoration.label, | ||
| labelText: effectiveDecoration.labelText, | ||
| labelStyle: effectiveDecoration.labelStyle, | ||
| floatingLabelStyle: effectiveDecoration.floatingLabelStyle, | ||
| helper: effectiveDecoration.helper, | ||
| helperText: effectiveDecoration.helperText, | ||
| helperStyle: effectiveDecoration.helperStyle, | ||
| helperMaxLines: effectiveDecoration.helperMaxLines, | ||
| hintText: effectiveDecoration.hintText, | ||
| hint: effectiveDecoration.hint, | ||
| hintStyle: effectiveDecoration.hintStyle, | ||
| hintTextDirection: effectiveDecoration.hintTextDirection, | ||
| hintFadeDuration: effectiveDecoration.hintFadeDuration, | ||
| hintMaxLines: effectiveDecoration.hintMaxLines, | ||
| errorStyle: effectiveDecoration.errorStyle, | ||
| errorMaxLines: effectiveDecoration.errorMaxLines, | ||
| floatingLabelBehavior: effectiveDecoration.floatingLabelBehavior, | ||
| floatingLabelAlignment: effectiveDecoration.floatingLabelAlignment, | ||
| isCollapsed: effectiveDecoration.isCollapsed, | ||
| isDense: effectiveDecoration.isDense, | ||
| contentPadding: effectiveDecoration.contentPadding, | ||
| prefixIcon: effectiveDecoration.prefixIcon, | ||
| prefix: effectiveDecoration.prefix, | ||
| prefixText: effectiveDecoration.prefixText, | ||
| prefixIconConstraints: effectiveDecoration.prefixIconConstraints, | ||
| prefixStyle: effectiveDecoration.prefixStyle, | ||
| prefixIconColor: effectiveDecoration.prefixIconColor, | ||
| suffixIcon: effectiveDecoration.suffixIcon, | ||
| suffix: effectiveDecoration.suffix, | ||
| suffixText: effectiveDecoration.suffixText, | ||
| suffixStyle: effectiveDecoration.suffixStyle, | ||
| suffixIconColor: effectiveDecoration.suffixIconColor, | ||
| suffixIconConstraints: effectiveDecoration.suffixIconConstraints, | ||
| counter: effectiveDecoration.counter, | ||
| counterText: effectiveDecoration.counterText, | ||
| counterStyle: effectiveDecoration.counterStyle, | ||
| filled: effectiveDecoration.filled, | ||
| fillColor: effectiveDecoration.fillColor, | ||
| focusColor: effectiveDecoration.focusColor, | ||
| hoverColor: effectiveDecoration.hoverColor, | ||
| errorBorder: effectiveDecoration.errorBorder, | ||
| focusedBorder: effectiveDecoration.focusedBorder, | ||
| focusedErrorBorder: effectiveDecoration.focusedErrorBorder, | ||
| disabledBorder: effectiveDecoration.disabledBorder, | ||
| enabledBorder: effectiveDecoration.enabledBorder, | ||
| border: effectiveDecoration.border, | ||
| enabled: effectiveDecoration.enabled, | ||
| semanticCounterText: effectiveDecoration.semanticCounterText, | ||
| alignLabelWithHint: effectiveDecoration.alignLabelWithHint, | ||
| constraints: effectiveDecoration.constraints, | ||
| visualDensity: effectiveDecoration.visualDensity, | ||
| ) |
There was a problem hiding this comment.
This large block for creating a new InputDecoration is very similar to the one added in dropdown.dart. This duplication makes the code harder to maintain.
To improve this, consider refactoring this logic into a shared helper function that can be used in both places. This would centralize the verbose constructor call and make future updates to InputDecoration easier to manage.
|
Could you update the description to match the PR Template? A much easier fix would be to just add an assert on these widgets to make sure both are not non-null in the constructor. That's a pattern we follow in other widgets as well You will also need to add tests that trigger the assert |
| errorText: errorText, | ||
| hintText: hintText, | ||
| ); | ||
| if (error != null) { |
There was a problem hiding this comment.
I believe it’s possible to use merge instead of recreating everything.
This PR fixes a framework crash that occurs when `errorBuilder` is used alongside an `InputDecoration` that contains an `errorText`. Because `InputDecoration.copyWith` ignores `null` values, attempting to clear the `errorText` dynamically during the build phase leaves the decoration with both properties defined, triggering a late assertion in `InputDecorator`. Following the architectural guidance provided by @Hari-07 in PR #183384, this fix enforces mutual exclusivity at the constructor level. It adds an `assert` to both `TextFormField` and `DropdownButtonFormField` to ensure developers fail fast if both properties are provided. Fixes #183378 I added the following tests to verify the assertions: - `TextFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. - `DropdownButtonFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. --------- Co-authored-by: Hari07 <22373191+Hari-07@users.noreply.github.com>
…#183901) This PR fixes a framework crash that occurs when `errorBuilder` is used alongside an `InputDecoration` that contains an `errorText`. Because `InputDecoration.copyWith` ignores `null` values, attempting to clear the `errorText` dynamically during the build phase leaves the decoration with both properties defined, triggering a late assertion in `InputDecorator`. Following the architectural guidance provided by @Hari-07 in PR flutter#183384, this fix enforces mutual exclusivity at the constructor level. It adds an `assert` to both `TextFormField` and `DropdownButtonFormField` to ensure developers fail fast if both properties are provided. Fixes flutter#183378 I added the following tests to verify the assertions: - `TextFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. - `DropdownButtonFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. --------- Co-authored-by: Hari07 <22373191+Hari-07@users.noreply.github.com>
…#183901) This PR fixes a framework crash that occurs when `errorBuilder` is used alongside an `InputDecoration` that contains an `errorText`. Because `InputDecoration.copyWith` ignores `null` values, attempting to clear the `errorText` dynamically during the build phase leaves the decoration with both properties defined, triggering a late assertion in `InputDecorator`. Following the architectural guidance provided by @Hari-07 in PR flutter#183384, this fix enforces mutual exclusivity at the constructor level. It adds an `assert` to both `TextFormField` and `DropdownButtonFormField` to ensure developers fail fast if both properties are provided. Fixes flutter#183378 I added the following tests to verify the assertions: - `TextFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. - `DropdownButtonFormField` asserts when both `errorBuilder` and `decoration.errorText` are provided. --------- Co-authored-by: Hari07 <22373191+Hari-07@users.noreply.github.com>
Fixes #183378. When using errorBuilder alongside a decoration that has errorText, both error and errorText end up non-null, triggering the assertion "Declaring both error and errorText is not supported." This happens because InputDecoration.copyWith() cannot clear fields (passing null means "keep existing value").
The fix explicitly creates a new InputDecoration with errorText set to null when errorBuilder is used, ensuring only one error display mechanism is active at a time.