-
Notifications
You must be signed in to change notification settings - Fork 28.5k
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
base: master
Are you sure you want to change the base?
Fix DropdownButtonFormField focusing when replacing FocusNode #166645
Conversation
There was a problem hiding this 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.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 inChangeNotifier
but itshasListeners
getter is marked as @Protected- The
_handleFocusChanged
callback doesn't affect anything anyway when leaked
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
88c52a5
to
900ed72
Compare
There was a problem hiding this 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!
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); |
There was a problem hiding this comment.
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.
Can you fix the analyzer failure here? |
…xternal FocusNode
f70f155
to
94698c7
Compare
All good now. |
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
focusNode = FocusNode(debugLabel: 'DropdownButtonFormField'); | ||
focusNode.requestFocus(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
fixes #166642
The newly added tests verify the following behaviors:
Pre-launch Checklist
///
).