-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[Focus] defer autofocus resolution to _applyFocusChange
#85562
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
Conversation
Fixes: flutter#84845. also fixes a potential crash in FocusState.didUpdateWdiget. when `FocusScopeNode.autofocus` is called, the FocusScopeNode may have a focusedChild that's about to be detached.
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.
On a different note, have we tried using LinkedHashList
for storing FocusNode._ancestors
and/or FocusScopeNode._focusedChildren
?
assert(_manager != null); | ||
assert(_focusDebug('Autofocus scheduled for $node: scope $this')); | ||
_manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node)); | ||
_manager?._markNeedsUpdate(); | ||
} | ||
|
||
@override | ||
void _doRequestFocus({required bool findFirstFocus}) { |
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.
This method still does the same thing. Just trying to avoid runtime type checks like primaryFocus is FocusScopeNode
.
@@ -1756,6 +1759,13 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { | |||
void _applyFocusChange() { | |||
_haveScheduledUpdate = false; | |||
final FocusNode? previousFocus = _primaryFocus; | |||
for (final _Autofocus autofocus in _pendingAutofocuses) { | |||
if (identical(autofocus.scope._manager, this)) { |
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.
This isn't going to work well if a subtree involved changes manager after calling FocusScopeNode.autofocus
since we keep the information in the manager. FocusManager
has a public constructor but I guess most of the time we only use the singleton from bindings.
@@ -657,7 +657,8 @@ class _FocusState extends State<Focus> { | |||
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable; | |||
} else { | |||
_focusAttachment!.detach(); | |||
focusNode.removeListener(_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.
This getter crashes if we supply an external FocusNode
in the old widget and do not specify one in the new widget.
|
||
assert(_manager != null); | ||
assert(_focusDebug('Autofocus scheduled for $node: scope $this')); | ||
_manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node)); |
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.
Should remove the node if it was disposed?
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.
The node
and the scope
? I think they're supposed to be owned by widgets so FocusManager
shouldn't call dispose on FocusNode
s.
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.
Yes, that's correct. The owner of the FocusNode
has the responsibility to dispose of it.
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 mean the node was disposed before applying the autofocus.
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.
Ah I see. But I'm not sure if there's a good way to check if a FocusNode
is already dispose
d. It seems fine to keep a dispose
d FocusNode
around? In _applyFocusChange
if either of these 2 nodes are detached from the tree then we throw the entry away anyways.
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.
If a node is disposed then it's guaranteed to be detached (reattaching disposed nodes back to the tree shouldn't be allowed). In _applyFocusChange
we do check if any of the nodes are already detached and discard them.
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.
That reminds me we didn't have tests to verify the first come first served behavior. I'll add that in a bit.
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.
If we scroll the list ,the primary focus node will be changed. A little strange I think.
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.
That's only true if you have autofocus
set to true on more than one widget, which, as I said above, isn't well defined (mainly because of situations like this where things are built later).
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.
Got it.
@@ -1756,6 +1759,13 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { | |||
void _applyFocusChange() { | |||
_haveScheduledUpdate = false; | |||
final FocusNode? previousFocus = _primaryFocus; | |||
for (final _Autofocus autofocus in _pendingAutofocuses) { | |||
if (identical(autofocus.scope._manager, this)) { |
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.
@LongCatIsLooong did you mean this line? For the test I posted this is true .
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.
Ohh right. I thought detaching removes the manager.
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.
Maybe we can do this.
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.
After looking at FocusNode.detach
, I think the idea is the widget tree (or whatever that the focus tree is going to attach to & and manage focus for) is responsible for calling "reparent" and "detach" so each focus node has the right parent
and manager
. So I think these checks should be sufficient.
@xu-baolin you mean this is true for something in the first row? I'd expect those elements to have already been disposed of (along with their focus nodes)?
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.
The node is disposed but the scope node not.
if (identical(autofocus.scope._manager, this) && autofocus.autofocusNode._parent!= null)
cd9e0f5
to
55d8d93
Compare
@xu-baolin I added some additional checks to make sure the scope is attached to the focus tree. |
), | ||
); | ||
|
||
// _applyFocusChanges will be called before persistentCallbacks, |
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.
- '_applyFocusChanges' should be '_applyFocusChange'.
- I remember the microTask should be called after the persistentCallbacks in a frame, right? Or do you mean before the next frame?
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.
The microtask queue is also drained before the persistent callbacks.
80b60f8
to
e5e233b
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.
LGTM
@LongCatIsLooong Are you still planning to commit this? |
Removing trailing whitespaces
@gspencergoog yeah but there appears to be an analyzer problem that I believe has nothing to do with the PR. Does the approach look good to you? |
e94c8ea
to
54b2025
Compare
@LongCatIsLooong Can we reland #82671 now?#84845 (comment) |
Fixes: #84845.
also fixes a potential crash in
FocusState.didUpdateWdiget
.when
FocusScopeNode.autofocus
is called, the FocusScopeNode may have afocusedChild that's about to be detached.
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.