Repository navigation
RawAutocomplete: Fix focusNode listener mismatch in didUpdateWidget/dispose - #190686
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates RawAutocompleteState to use _onFocusChange instead of _updateOptionsViewVisibility as the listener for focus node changes, and adds a test to verify that the listener is removed from a caller-provided focus node upon disposal. The review feedback identifies an issue where transitioning from a custom focus node to a null focus node leaves the newly created internal focus node without the _onFocusChange listener, and provides a code suggestion to resolve this.
| oldWidget.focusNode?.removeListener(_onFocusChange); | ||
| if (oldWidget.focusNode == null) { | ||
| _internalFocusNode?.dispose(); | ||
| _internalFocusNode = null; | ||
| } | ||
| widget.focusNode?.addListener(_updateOptionsViewVisibility); | ||
| widget.focusNode?.addListener(_onFocusChange); |
There was a problem hiding this comment.
When transitioning from a custom focusNode (non-null) to the default focusNode (null), oldWidget.focusNode is not null, so the if (oldWidget.focusNode == null) block is skipped.
Since widget.focusNode is null, widget.focusNode?.addListener(_onFocusChange) is a no-op. As a result, _internalFocusNode remains null until it is lazily initialized by the _focusNode getter. However, when it is lazily initialized, the _onFocusChange listener is never added to it. This means the autocomplete widget will not respond to focus changes on the newly created internal focus node.
To fix this, we should initialize _internalFocusNode and add the listener when transitioning from a custom focus node to null.
oldWidget.focusNode?.removeListener(_onFocusChange);
if (oldWidget.focusNode == null) {
_internalFocusNode?.dispose();
_internalFocusNode = null;
} else if (widget.focusNode == null) {
_internalFocusNode = FocusNode()..addListener(_onFocusChange);
}
widget.focusNode?.addListener(_onFocusChange);There was a problem hiding this comment.
No change needed. Gemini seems to have hallucinated this claim:
However, when it is lazily initialized, the _onFocusChange listener is never added to it.
The listener is added when the internal node is lazily initialized, in the _focusNode getter:
FocusNode get _focusNode {
return widget.focusNode ?? (_internalFocusNode ??= FocusNode()..addListener(_onFocusChange));
}so a rebuild from a custom focusNode to null is covered: the next _focusNode access creates the internal node with the listener already attached. (_internalTextEditingController follows the same pattern.) This PR also doesn't touch that logic; it only corrects which listener didUpdateWidget and dispose remove.
There was a problem hiding this comment.
/gemini can you check this again?
There was a problem hiding this comment.
You are correct, and I apologize for the confusion. I misread the _focusNode getter implementation. Since _focusNode lazily initializes _internalFocusNode and attaches the _onFocusChange listener upon first access, the transition from a custom focusNode to null is indeed handled correctly by the existing logic. My previous concern about the listener not being added was unfounded. Thank you for pointing this out.
victorsanni
left a comment
There was a problem hiding this comment.
Approach LGTM but left some comments.
justinmc
left a comment
There was a problem hiding this comment.
The approach looks good to me but +1 to @victorsanni's comments. Thanks for the continued work on Autocomplete @chrisbobbe !
…ispose initState registers _onFocusChange on the passed FocusNode, but didUpdateWidget and dispose were removing/adding _updateOptionsViewVisibility instead. This meant the _onFocusChange listener was never properly removed, so it could fire on an already-disposed State. Also refresh _hasFocus when the focus node changes, since the new node's focus state may not match the old node's, and add tests covering focus-node swaps.
8186f2c to
68d850d
Compare
|
Thanks @victorsanni and @justinmc for the reviews! Revision pushed; now |
|
@victorsanni it looks like you've approved this; thanks for the review! Would you re-add the CICD label please? |
|
Gentle bump on this, which is part of my work toward fixing #159443. 🙂 |
justinmc
left a comment
There was a problem hiding this comment.
LGTM 👍 . Sorry for the delay here @chrisbobbe!
| expect(focusNode2.hasFocus, isFalse); | ||
| expect(focusNode1.hasListeners, isFalse); |
There was a problem hiding this comment.
Nit: Consider also expecting the focusNode1.hasFocus and focusNode2.hasListeners.
There was a problem hiding this comment.
Do you mean like this?
diff --git packages/flutter/test/widgets/autocomplete_test.dart packages/flutter/test/widgets/autocomplete_test.dart
index d09a4d030..d6e09528a 100644
--- packages/flutter/test/widgets/autocomplete_test.dart
+++ packages/flutter/test/widgets/autocomplete_test.dart
@@ -4109,8 +4109,10 @@ void main() {
// Swap in a focus node that, unlike the old one, is unfocused.
await pumpRawAutocomplete(focusNode2);
await tester.pump();
+ expect(focusNode2.hasListeners, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(focusNode1.hasListeners, isFalse);
+ expect(focusNode1.hasFocus, isTrue);
// Make options available again, without changing the focus state.
textEditingController.text = '';The second new expect, expect(focusNode1.hasFocus, isTrue), is meant to correspond to "unlike the old one" in the code comment. But it fails in that position, actually. I'm not sure I would've expected that. It passes if I move it before the await pumpRawAutocomplete(focusNode2);.
It looks like the replacement of focusNode1 causes focusNode1 to become unfocused, as part of being "detached". Here's a dump of the logs when I run just this one test with debugFocusChanges on:
diff --git packages/flutter/test/widgets/autocomplete_test.dart packages/flutter/test/widgets/autocomplete_test.dart
index d09a4d030..053fa0e4c 100644
--- packages/flutter/test/widgets/autocomplete_test.dart
+++ packages/flutter/test/widgets/autocomplete_test.dart
@@ -4107,10 +4107,17 @@ void main() {
expect(find.byKey(optionsKey), findsNothing);
// Swap in a focus node that, unlike the old one, is unfocused.
+ print('focusNode1: $focusNode1');
+ print('focusNode2: $focusNode2');
+ print('---------');
+ debugFocusChanges = true;
await pumpRawAutocomplete(focusNode2);
+ debugFocusChanges = false;
await tester.pump();
+ expect(focusNode2.hasListeners, isTrue);
expect(focusNode2.hasFocus, isFalse);
expect(focusNode1.hasListeners, isFalse);
+ expect(focusNode1.hasFocus, isTrue);
// Make options available again, without changing the focus state.
textEditingController.text = '';Details
$ flutter test packages/flutter/test/widgets/autocomplete_test.dart --name 'refreshes the internal focus state when the focusNode changes'
00:01 +0: refreshes the internal focus state when the focusNode changes
focusNode1: _SpyFocusNode#12d50([PRIMARY FOCUS])(context: Focus, PRIMARY FOCUS)
focusNode2: _SpyFocusNode#298a1
---------
FOCUS: Detaching node:
_SpyFocusNode#12d50([PRIMARY FOCUS])(context: Focus, PRIMARY FOCUS)
With enclosing scope FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [_SpyFocusNode#12d50([PRIMARY FOCUS])], traversalEdgeBehavior: TraversalEdgeBehavior.parentScope)
FOCUS: Setting FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH) as focused child for scope:
FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])(context: FocusScope, IN FOCUS PATH, focusedChildren: [FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])])
FOCUS: Setting FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])(context: FocusScope, IN FOCUS PATH, focusedChildren: [FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])]) as focused child for scope:
FocusScopeNode#97929(View Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])])
FOCUS: Setting FocusScopeNode#97929(View Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])]) as focused child for scope:
FocusScopeNode#4f83e(Root Focus Scope [IN FOCUS PATH])(IN FOCUS PATH, focusedChildren: [FocusScopeNode#97929(View Scope [IN FOCUS PATH])])
FOCUS: Scheduling update, current focus is _SpyFocusNode#12d50([PRIMARY FOCUS])(context: Focus, PRIMARY FOCUS), next focus will be FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH)
FOCUS: Unfocused node:
primary focus was _SpyFocusNode#12d50([PRIMARY FOCUS])(context: Focus, PRIMARY FOCUS)
next focus will be FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH)
FOCUS: Node was detached: _SpyFocusNode#12d50([PRIMARY FOCUS])(context: Focus, PRIMARY FOCUS)
FOCUS: Refreshing focus state. Next focus will be FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope)(context: _FocusScopeWithExternalFocusNode)
FOCUS: Updating focus from null to FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])(context: _FocusScopeWithExternalFocusNode, PRIMARY FOCUS)
FOCUS: Setting FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])(context: _FocusScopeWithExternalFocusNode, PRIMARY FOCUS) as focused child for scope:
FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])(context: FocusScope, IN FOCUS PATH, focusedChildren: [FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])])
FOCUS: Setting FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])(context: FocusScope, IN FOCUS PATH, focusedChildren: [FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])]) as focused child for scope:
FocusScopeNode#97929(View Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])])
FOCUS: Setting FocusScopeNode#97929(View Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])]) as focused child for scope:
FocusScopeNode#4f83e(Root Focus Scope [IN FOCUS PATH])(IN FOCUS PATH, focusedChildren: [FocusScopeNode#97929(View Scope [IN FOCUS PATH])])
FOCUS: Notified 12 dirty nodes:
FocusNode#aa2a8(Navigator [IN FOCUS PATH])(context: Focus, IN FOCUS PATH)
_FocusTraversalGroupNode#784f9(FocusTraversalGroup [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])(context: FocusScope, IN FOCUS PATH, focusedChildren: [FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])])
FocusNode#b60f8([IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusNode#1e109(Shortcuts: <Shortcut Registrar> [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
_FocusTraversalGroupNode#a4e43(FocusTraversalGroup [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusNode#b8e8a(Shortcuts: <Default Text Editing Shortcuts> [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusNode#2c86b(Shortcuts: <Default WidgetsApp Shortcuts> [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusScopeNode#97929(View Scope [IN FOCUS PATH])(context: _FocusScopeWithExternalFocusNode, IN FOCUS PATH, focusedChildren: [FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])])
_FocusTraversalGroupNode#d767d(FocusTraversalGroup [IN FOCUS PATH])(context: Focus, NOT FOCUSABLE, IN FOCUS PATH)
FocusScopeNode#4f83e(Root Focus Scope [IN FOCUS PATH])(IN FOCUS PATH, focusedChildren: [FocusScopeNode#97929(View Scope [IN FOCUS PATH])])
FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])(context: _FocusScopeWithExternalFocusNode, PRIMARY FOCUS)
FocusManager#87b36
│ primaryFocus: FocusScopeNode#cb1fb(_ModalScopeState<dynamic>
│ Focus Scope [PRIMARY FOCUS])
│ primaryFocusCreator: _FocusScopeWithExternalFocusNode ←
│ PrimaryScrollController ← _ActionsScope ← Actions ← Builder ←
│ PageStorage ← Offstage ← _ModalScopeStatus ←
│ UnmanagedRestorationScope ← RestorationScope ← AnimatedBuilder
│ ←
│ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#8921d]
│ ← Semantics ← Builder ← _RenderTheaterMarker ←
│ _EffectiveTickerMode ← TickerMode ←
│ _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#cf444]
│ ← _Theater ← Overlay-[LabeledGlobalKey<OverlayState>#d01e2] ← ⋯
│
└─rootScope: FocusScopeNode#4f83e(Root Focus Scope [IN FOCUS PATH])
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#97929(View Scope [IN FOCUS PATH])
│
└─Child 1: _FocusTraversalGroupNode#d767d(FocusTraversalGroup [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusScopeNode#97929(View Scope [IN FOCUS PATH])
│ context: _FocusScopeWithExternalFocusNode
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#701c8(Navigator Scope [IN FOCUS
│ PATH])
│
└─Child 1: FocusNode#2c86b(Shortcuts: <Default WidgetsApp Shortcuts> [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusNode#b8e8a(Shortcuts: <Default Text Editing Shortcuts> [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: _FocusTraversalGroupNode#a4e43(FocusTraversalGroup [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusNode#1e109(Shortcuts: <Shortcut Registrar> [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusNode#b60f8([IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusScopeNode#701c8(Navigator Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#cb1fb(_ModalScopeState<dynamic>
│ Focus Scope [PRIMARY FOCUS])
│
└─Child 1: _FocusTraversalGroupNode#784f9(FocusTraversalGroup [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusNode#aa2a8(Navigator [IN FOCUS PATH])
│ context: Focus
│ IN FOCUS PATH
│
└─Child 1: FocusScopeNode#cb1fb(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])
│ context: _FocusScopeWithExternalFocusNode
│ PRIMARY FOCUS
│
└─Child 1: FocusNode#193e8(Shortcuts)
│ context: Focus
│ NOT FOCUSABLE
│
└─Child 1: _SpyFocusNode#298a1
context: Focus
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: true
Actual: <false>
When the exception was thrown, this was the stack:
#4 main.<anonymous closure> (file:///Users/chrisbobbe/.local/lib/flutter/packages/flutter/test/widgets/autocomplete_test.dart:4120:5)
<asynchronous suspension>
#5 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:192:15)
<asynchronous suspension>
#6 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:2062:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
This was caught by the test expectation on the following line:
file:///Users/chrisbobbe/.local/lib/flutter/packages/flutter/test/widgets/autocomplete_test.dart line 4120
The test description was:
refreshes the internal focus state when the focusNode changes
════════════════════════════════════════════════════════════════════════════════════════════════════
00:01 +0 -1: refreshes the internal focus state when the focusNode changes [E]
Test failed. See exception logs above.
The test description was: refreshes the internal focus state when the focusNode changes
To run this test again: /Users/chrisbobbe/.local/lib/flutter/bin/cache/dart-sdk/bin/dart test /Users/chrisbobbe/.local/lib/flutter/packages/flutter/test/widgets/autocomplete_test.dart -p vm --plain-name 'refreshes the internal focus state when the focusNode changes'
00:01 +0 -1: Some tests failed.
See in particular "primary focus was [focusNode1]" / "next focus will be [focusNode2]", under "FOCUS: Unfocused node:".
I think this test doesn't necessarily need to pin focusNode1's state after the switch. The important thing after the switch is that the new internal focus state matches the incoming focusNode2's state. In my next revision, I'll add the expect(focusNode2.hasListeners, isTrue);, though, as a sanity check, because it would be clearly wrong if focusNode2 had no listeners after the switch.
There was a problem hiding this comment.
I think the reason focusNode1 doesn't have focus is that it has been disposed. That's kind of irrelevant to what you're trying to test, so I'm on board with omitting that check.
|
autosubmit label was removed for flutter/flutter/190686, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR. |
…c's review See: flutter#190686 (comment) and my reply there.
|
Thanks for the review! Revision pushed, and please see my reply inline above: #190686 (comment) |
| expect(focusNode2.hasFocus, isFalse); | ||
| expect(focusNode1.hasListeners, isFalse); |
There was a problem hiding this comment.
I think the reason focusNode1 doesn't have focus is that it has been disposed. That's kind of irrelevant to what you're trying to test, so I'm on board with omitting that check.
|
Thanks for the reviews! |
flutter/flutter@426be96...5c7e45a 2026-10-15 [email protected] [web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional (flutter/flutter#193736) 2026-10-15 [email protected] Roll Skia from 79a00d7c4898 to d131b7789482 (2 revisions) (flutter/flutter#194446) 2026-10-15 [email protected] Roll Dart SDK from b8ffa9233096 to 84686ea04815 (1 revision) (flutter/flutter#194442) 2026-10-15 [email protected] Roll Dart SDK from e81af2dac9d1 to b8ffa9233096 (1 revision) (flutter/flutter#194432) 2026-10-15 [email protected] Roll Skia from ef24b8f87a2a to 79a00d7c4898 (4 revisions) (flutter/flutter#194431) 2026-10-15 [email protected] Roll Skia from 48c51004ee4e to ef24b8f87a2a (1 revision) (flutter/flutter#194427) 2026-10-15 [email protected] Roll Dart SDK from 6b50116630d0 to e81af2dac9d1 (5 revisions) (flutter/flutter#194422) 2026-10-15 [email protected] Roll Skia from b1be97266d3a to 48c51004ee4e (10 revisions) (flutter/flutter#194421) 2026-10-15 [email protected] RawAutocomplete: Fix focusNode listener mismatch in didUpdateWidget/dispose (flutter/flutter#190686) 2026-10-14 [email protected] [flutter_tools] Wait for iOS simulator log stream before launch (flutter/flutter#193142) 2026-10-14 [email protected] Move Material/Cupertino PRs from design triage to framework triage (flutter/flutter#194244) 2026-10-14 [email protected] refactor(flutter_tools): share FlutterCommand options and OptionBundles (flutter/flutter#194356) 2026-10-14 [email protected] Roll pub packages (flutter/flutter#194413) 2026-10-14 [email protected] Add ContextMenuButtonKind (flutter/flutter#193528) 2026-10-14 [email protected] Split SwiftPM plugin into multiple products (flutter/flutter#193521) 2026-10-14 [email protected] Roll Skia from 43d707a3be0e to b1be97266d3a (8 revisions) (flutter/flutter#194392) 2026-10-14 [email protected] Roll pub packages (flutter/flutter#194398) 2026-10-14 [email protected] [flutter_tools] Migrate AndroidSdk and AndroidStudio away from globals (flutter/flutter#193650) 2026-10-14 [email protected] [flutter_tools] Reload widget-preview when data assets change (flutter/flutter#193437) 2026-10-14 [email protected] [flutter tool] suppress sub-tool analytics during first run (flutter/flutter#194320) 2026-10-14 [email protected] Roll Skia from acde3755d2e2 to 43d707a3be0e (1 revision) (flutter/flutter#194385) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…ispose (flutter#190686) This is a minor bugfix I'm proposing as prep for my work on flutter#159443. For details on how that work blocks on this, see flutter#183705 (comment). initState registers _onFocusChange on the passed FocusNode, but didUpdateWidget and dispose were removing/adding _updateOptionsViewVisibility instead. This meant the _onFocusChange listener was never properly removed, so it could fire on an already-disposed State. Also, when the focus node changes, refresh _hasFocus from the new node, since its focus state may not match the old node's. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [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]. - [ ] I listed at least one issue that this PR fixes in the description above. - [x] 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]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [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 --------- Co-authored-by: Victor Sanni <[email protected]>
This is a minor bugfix I'm proposing as prep for my work on #159443. For details on how that work blocks on this, see #183705 (comment).
initState registers _onFocusChange on the passed FocusNode, but didUpdateWidget and dispose were removing/adding
_updateOptionsViewVisibility instead. This meant the _onFocusChange listener was never properly removed, so it could fire on an already-disposed State.
Also, when the focus node changes, refresh _hasFocus from the new node, since its focus state may not match the old node's.
Pre-launch Checklist
///).Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.