Skip to content

Commit dddbd04

Browse files
Reland "Cache FocusNode.enclosingScope, clean up descendantsAreFocusable (flutter#144207)" (flutter#144330)
The [internal test failure](flutter#144207 (comment)) was caused by `Focus.withExternalFocusNode` modifying the external node's attributes. The extra changes are in this commit: flutter@e53d98b CL with (almost) passing TGP: cl/611157582
1 parent 7f65c9e commit dddbd04

File tree

4 files changed

+113
-39
lines changed

4 files changed

+113
-39
lines changed

packages/flutter/lib/src/widgets/focus_manager.dart

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -517,21 +517,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
517517
/// focus traversal policy for a widget subtree.
518518
/// * [FocusTraversalPolicy], a class that can be extended to describe a
519519
/// traversal policy.
520-
bool get canRequestFocus {
521-
if (!_canRequestFocus) {
522-
return false;
523-
}
524-
final FocusScopeNode? scope = enclosingScope;
525-
if (scope != null && !scope.canRequestFocus) {
526-
return false;
527-
}
528-
for (final FocusNode ancestor in ancestors) {
529-
if (!ancestor.descendantsAreFocusable) {
530-
return false;
531-
}
532-
}
533-
return true;
534-
}
520+
bool get canRequestFocus => _canRequestFocus && ancestors.every(_allowDescendantsToBeFocused);
521+
static bool _allowDescendantsToBeFocused(FocusNode ancestor) => ancestor.descendantsAreFocusable;
535522

536523
bool _canRequestFocus;
537524
@mustCallSuper
@@ -791,6 +778,22 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
791778
/// Use [enclosingScope] to look for scopes above this node.
792779
FocusScopeNode? get nearestScope => enclosingScope;
793780

781+
FocusScopeNode? _enclosingScope;
782+
void _clearEnclosingScopeCache() {
783+
final FocusScopeNode? cachedScope = _enclosingScope;
784+
if (cachedScope == null) {
785+
return;
786+
}
787+
_enclosingScope = null;
788+
if (children.isNotEmpty) {
789+
for (final FocusNode child in children) {
790+
if (identical(cachedScope, child._enclosingScope)) {
791+
child._clearEnclosingScopeCache();
792+
}
793+
}
794+
}
795+
}
796+
794797
/// Returns the nearest enclosing scope node above this node, or null if the
795798
/// node has not yet be added to the focus tree.
796799
///
@@ -799,12 +802,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
799802
///
800803
/// Use [nearestScope] to start at this node instead of above it.
801804
FocusScopeNode? get enclosingScope {
802-
for (final FocusNode node in ancestors) {
803-
if (node is FocusScopeNode) {
804-
return node;
805-
}
806-
}
807-
return null;
805+
final FocusScopeNode? enclosingScope = _enclosingScope ??= parent?.nearestScope;
806+
assert(enclosingScope == parent?.nearestScope, '$this has invalid scope cache: $_enclosingScope != ${parent?.nearestScope}');
807+
return enclosingScope;
808808
}
809809

810810
/// Returns the size of the attached widget's [RenderObject], in logical
@@ -990,6 +990,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
990990
}
991991

992992
node._parent = null;
993+
node._clearEnclosingScopeCache();
993994
_children.remove(node);
994995
for (final FocusNode ancestor in ancestors) {
995996
ancestor._descendants = null;
@@ -1268,13 +1269,14 @@ class FocusScopeNode extends FocusNode {
12681269
super.skipTraversal,
12691270
super.canRequestFocus,
12701271
this.traversalEdgeBehavior = TraversalEdgeBehavior.closedLoop,
1271-
}) : super(
1272-
descendantsAreFocusable: true,
1273-
);
1272+
}) : super(descendantsAreFocusable: true);
12741273

12751274
@override
12761275
FocusScopeNode get nearestScope => this;
12771276

1277+
@override
1278+
bool get descendantsAreFocusable => _canRequestFocus && super.descendantsAreFocusable;
1279+
12781280
/// Controls the transfer of focus beyond the first and the last items of a
12791281
/// [FocusScopeNode].
12801282
///

packages/flutter/lib/src/widgets/focus_scope.dart

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ class _FocusWithExternalFocusNode extends Focus {
511511

512512
class _FocusState extends State<Focus> {
513513
FocusNode? _internalNode;
514-
FocusNode get focusNode => widget.focusNode ?? _internalNode!;
514+
FocusNode get focusNode => widget.focusNode ?? (_internalNode ??= _createNode());
515515
late bool _hadPrimaryFocus;
516516
late bool _couldRequestFocus;
517517
late bool _descendantsWereFocusable;
@@ -526,17 +526,13 @@ class _FocusState extends State<Focus> {
526526
}
527527

528528
void _initNode() {
529-
if (widget.focusNode == null) {
530-
// Only create a new node if the widget doesn't have one.
531-
// This calls a function instead of just allocating in place because
532-
// _createNode is overridden in _FocusScopeState.
533-
_internalNode ??= _createNode();
534-
}
535-
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
536-
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
537-
focusNode.skipTraversal = widget.skipTraversal;
538-
if (widget._canRequestFocus != null) {
539-
focusNode.canRequestFocus = widget._canRequestFocus!;
529+
if (!widget._usingExternalFocus) {
530+
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
531+
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
532+
focusNode.skipTraversal = widget.skipTraversal;
533+
if (widget._canRequestFocus != null) {
534+
focusNode.canRequestFocus = widget._canRequestFocus!;
535+
}
540536
}
541537
_couldRequestFocus = focusNode.canRequestFocus;
542538
_descendantsWereFocusable = focusNode.descendantsAreFocusable;

packages/flutter/test/widgets/focus_manager_test.dart

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,16 +529,16 @@ void main() {
529529
// child1
530530
// |
531531
// child2
532-
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope2');
532+
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
533533
addTearDown(scope1.dispose);
534534
final FocusAttachment scope2Attachment = scope1.attach(context);
535535
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
536536

537-
final FocusNode child1 = FocusNode(debugLabel: 'child2');
537+
final FocusNode child1 = FocusNode(debugLabel: 'child1');
538538
addTearDown(child1.dispose);
539539
final FocusAttachment child2Attachment = child1.attach(context);
540540

541-
final FocusNode child2 = FocusNode(debugLabel: 'child3');
541+
final FocusNode child2 = FocusNode(debugLabel: 'child2');
542542
addTearDown(child2.dispose);
543543
final FocusAttachment child3Attachment = child2.attach(context);
544544

@@ -697,6 +697,26 @@ void main() {
697697
expect(parent2.children.first, equals(child1));
698698
});
699699

700+
test('FocusScopeNode.canRequestFocus affects descendantsAreFocusable', () {
701+
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
702+
703+
scope.descendantsAreFocusable = false;
704+
expect(scope.descendantsAreFocusable, isFalse);
705+
expect(scope.canRequestFocus, isTrue);
706+
707+
scope.descendantsAreFocusable = true;
708+
expect(scope.descendantsAreFocusable, isTrue);
709+
expect(scope.canRequestFocus, isTrue);
710+
711+
scope.canRequestFocus = false;
712+
expect(scope.descendantsAreFocusable, isFalse);
713+
expect(scope.canRequestFocus, isFalse);
714+
715+
scope.canRequestFocus = true;
716+
expect(scope.descendantsAreFocusable, isTrue);
717+
expect(scope.canRequestFocus, isTrue);
718+
});
719+
700720
testWidgets('canRequestFocus affects children.', (WidgetTester tester) async {
701721
final BuildContext context = await setupWidget(tester);
702722
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');

packages/flutter/test/widgets/focus_scope_test.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,21 @@ void main() {
19641964
expect(keyEventHandled, isTrue);
19651965
});
19661966

1967+
testWidgets('Focus does not update the focusNode attributes when the widget updates if withExternalFocusNode is used 2', (WidgetTester tester) async {
1968+
final TestExternalFocusNode focusNode = TestExternalFocusNode();
1969+
assert(!focusNode.isModified);
1970+
addTearDown(focusNode.dispose);
1971+
1972+
final Focus focusWidget = Focus.withExternalFocusNode(
1973+
focusNode: focusNode,
1974+
child: Container(),
1975+
);
1976+
1977+
await tester.pumpWidget(focusWidget);
1978+
expect(focusNode.isModified, isFalse);
1979+
await tester.pumpWidget(const SizedBox());
1980+
});
1981+
19671982
testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async {
19681983
await tester.pumpWidget(
19691984
Focus(
@@ -2194,3 +2209,44 @@ class TestFocusState extends State<TestFocus> {
21942209
);
21952210
}
21962211
}
2212+
2213+
class TestExternalFocusNode extends FocusNode {
2214+
TestExternalFocusNode();
2215+
2216+
bool isModified = false;
2217+
2218+
@override
2219+
FocusOnKeyEventCallback? get onKeyEvent => _onKeyEvent;
2220+
FocusOnKeyEventCallback? _onKeyEvent;
2221+
@override
2222+
set onKeyEvent(FocusOnKeyEventCallback? newValue) {
2223+
if (newValue != _onKeyEvent) {
2224+
_onKeyEvent = newValue;
2225+
isModified = true;
2226+
}
2227+
}
2228+
2229+
@override
2230+
set descendantsAreFocusable(bool newValue) {
2231+
super.descendantsAreFocusable = newValue;
2232+
isModified = true;
2233+
}
2234+
2235+
@override
2236+
set descendantsAreTraversable(bool newValue) {
2237+
super.descendantsAreTraversable = newValue;
2238+
isModified = true;
2239+
}
2240+
2241+
@override
2242+
set skipTraversal(bool newValue) {
2243+
super.skipTraversal = newValue;
2244+
isModified = true;
2245+
}
2246+
2247+
@override
2248+
set canRequestFocus(bool newValue) {
2249+
super.canRequestFocus = newValue;
2250+
isModified = true;
2251+
}
2252+
}

0 commit comments

Comments
 (0)