Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix DropdownButtonFormField focusing when replacing FocusNode #166645

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

AhmedLSayed9
Copy link
Contributor

fixes #166642

The newly added tests verify the following behaviors:

  1. DropdownButtonFormField can be focused if it was unfocused and we replaced FocusNode.
  2. DropdownButtonFormField can be unfocused if it was focused and we replaced FocusNode.

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Apr 5, 2025
@dkwingsmt dkwingsmt requested a review from justinmc April 9, 2025 18:25
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for jumping in to fix this! I think I see a possible leak, see my comment below. Otherwise this looks good.

Comment on lines 1360 to 1371
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode?.removeListener(_handleFocusChanged);
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
}
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
focusNode.addListener(_handleFocusChanged);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be possible for _internalNode to leak a listener. Consider if oldWidget had no focusNode, so it created an _internalNode in initState. Then here there is a widget.focusNode. We should call _internalNode.removeListener, but it looks like we don't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write a test that covers this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've handled that case.

I can't find a way to test it, since:

  • FocusNode mixes in ChangeNotifier but its hasListeners getter is marked as @Protected
  • The _handleFocusChanged callback doesn't affect anything anyway when leaked

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some automatic leak detection that might catch this. You can run it like:

flutter test --dart-define LEAK_TRACKING=true <path/to/test.dart>

So I think if you just write a test that covers this case it's good enough, even if you can't directly verify that a leak didn't happen. Assuming we don't already have a test that covers this, I would do something like:

  • Build a DropdownButtonFormField wrapped in a StatefulBuilder with some boolean to decide whether to pass a focusNode. Start off with no focusNode.
  • Maybe expect that it uses focusColor when focused or something.
  • Call change your boolean and call setState so it rebuilds and now has no focusNode.
  • Expect the focusColor thing again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running the test with LEAK_TRACKING=true revealed that a FocusNode is still leaking due to not being properly disposed. I found that we should therefore fully dispose _internalNode rather than just removing the listener.

Interestingly, running the test with or without disposing _internalNode doesn't trigger a leak warning. The warning only appears when we remove the listener without disposing the focus node.

Given that we're now disposing the internal focus node, we can write a more relevant test to cover that, by calling the dispose method again and verifying that it throws an error.

@AhmedLSayed9 AhmedLSayed9 requested a review from justinmc April 18, 2025 22:11
@AhmedLSayed9 AhmedLSayed9 force-pushed the fix_dropdown_focus_node_replace branch from 88c52a5 to 900ed72 Compare April 18, 2025 22:15
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two possible changes to make in my two comments, otherwise good. Thanks for the quick response!

Comment on lines 1360 to 1371
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode?.removeListener(_handleFocusChanged);
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
}
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
focusNode.addListener(_handleFocusChanged);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some automatic leak detection that might catch this. You can run it like:

flutter test --dart-define LEAK_TRACKING=true <path/to/test.dart>

So I think if you just write a test that covers this case it's good enough, even if you can't directly verify that a leak didn't happen. Assuming we don't already have a test that covers this, I would do something like:

  • Build a DropdownButtonFormField wrapped in a StatefulBuilder with some boolean to decide whether to pass a focusNode. Start off with no focusNode.
  • Maybe expect that it uses focusColor when focused or something.
  • Call change your boolean and call setState so it rebuilds and now has no focusNode.
  • Expect the focusColor thing again.

@AhmedLSayed9 AhmedLSayed9 requested a review from justinmc April 19, 2025 04:51
@justinmc
Copy link
Contributor

Can you fix the analyzer failure here?

@AhmedLSayed9 AhmedLSayed9 force-pushed the fix_dropdown_focus_node_replace branch from f70f155 to 94698c7 Compare April 21, 2025 22:05
@AhmedLSayed9
Copy link
Contributor Author

Can you fix the analyzer failure here?

All good now.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay, one question about the tests.

focusNode.requestFocus();

await tester.pumpWidget(buildFormField());
await tester.pump(); // Wait for requestFocus to take effect.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for including the comment here.

Comment on lines +2607 to +2608
focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
focusNode.requestFocus();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this, why do you create a new FocusNode and not use it anywhere? The widget created by buildFormField will not have a reference to this new FocusNode right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't create a new FocusNode variable, it replaces the value of the existing focusNode variable which is used by buildFormField. The focusNode declared at the start of the test is a mutable variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DropdownButtonFormField can't be focused/unfocused when replacing FocusNode
3 participants