Skip to content

Commit 7aec9b4

Browse files
authored
Avoid calling didChangeDependences on a State that has dropped out of the tree (flutter#49527)
1 parent 11c5812 commit 7aec9b4

4 files changed

Lines changed: 95 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
657657
/// Has no effect on nodes that return true from [hasFocus], but false from
658658
/// [hasPrimaryFocus].
659659
///
660-
/// if [focusPrevious] is true, then rather than losing all focus, the focus
660+
/// If [focusPrevious] is true, then rather than losing all focus, the focus
661661
/// will be moved to the node that the [enclosingScope] thinks should have it,
662662
/// based on its history of nodes that were set as first focus on it using
663663
/// [FocusScopeNode.setFirstFocus].

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,13 @@ class _FocusState extends State<Focus> {
398398
@override
399399
void deactivate() {
400400
super.deactivate();
401+
// The focus node's location in the tree is no longer valid here. But
402+
// we can't unfocus or remove the node from the tree because if the widget
403+
// is moved to a different part of the tree (via global key) it should
404+
// retain its focus state. That's why we temporarily park it on the root
405+
// focus node (via reparent) until it either gets moved to a different part
406+
// of the tree (via didChangeDependencies) or until it is disposed.
407+
_focusAttachment?.reparent();
401408
_didAutofocus = false;
402409
}
403410

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4437,7 +4437,13 @@ class StatefulElement extends ComponentElement {
44374437
}
44384438

44394439
@override
4440-
Widget build() => state.build(this);
4440+
Widget build() {
4441+
if (_didChangeDependencies) {
4442+
_state.didChangeDependencies();
4443+
_didChangeDependencies = false;
4444+
}
4445+
return state.build(this);
4446+
}
44414447

44424448
/// The [State] instance associated with this location in the tree.
44434449
///
@@ -4617,10 +4623,21 @@ class StatefulElement extends ComponentElement {
46174623
return super.dependOnInheritedElement(ancestor as InheritedElement, aspect: aspect);
46184624
}
46194625

4626+
/// This controls whether we should call [State.didChangeDependencies] from
4627+
/// the start of [build], to avoid calls when the [State] will not get built.
4628+
/// This can happen when the widget has dropped out of the tree, but depends
4629+
/// on an [InheritedWidget] that is still in the tree.
4630+
///
4631+
/// It is set initially to false, since [_firstBuild] makes the initial call
4632+
/// on the [state]. When it is true, [build] will call
4633+
/// `state.didChangeDependencies` and then sets it to false. Subsequent calls
4634+
/// to [didChangeDependencies] set it to true.
4635+
bool _didChangeDependencies = false;
4636+
46204637
@override
46214638
void didChangeDependencies() {
46224639
super.didChangeDependencies();
4623-
_state.didChangeDependencies();
4640+
_didChangeDependencies = true;
46244641
}
46254642

46264643
@override

packages/flutter/test/widgets/framework_test.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,35 @@ void main() {
710710
);
711711
}
712712
});
713+
714+
testWidgets('didUpdateDependencies is not called on a State that never rebuilds', (WidgetTester tester) async {
715+
final GlobalKey<DependentState> key = GlobalKey<DependentState>();
716+
717+
/// Initial build - should call didChangeDependencies, not deactivate
718+
await tester.pumpWidget(Inherited(1, child: DependentStatefulWidget(key: key)));
719+
final DependentState state = key.currentState;
720+
expect(key.currentState, isNotNull);
721+
expect(state.didChangeDependenciesCount, 1);
722+
expect(state.deactivatedCount, 0);
723+
724+
/// Rebuild with updated value - should call didChangeDependencies
725+
await tester.pumpWidget(Inherited(2, child: DependentStatefulWidget(key: key)));
726+
expect(key.currentState, isNotNull);
727+
expect(state.didChangeDependenciesCount, 2);
728+
expect(state.deactivatedCount, 0);
729+
730+
// reparent it - should call deactivate and didChangeDependencies
731+
await tester.pumpWidget(Inherited(3, child: SizedBox(child: DependentStatefulWidget(key: key))));
732+
expect(key.currentState, isNotNull);
733+
expect(state.didChangeDependenciesCount, 3);
734+
expect(state.deactivatedCount, 1);
735+
736+
// Remove it - should call deactivate, but not didChangeDependencies
737+
await tester.pumpWidget(const Inherited(4, child: SizedBox()));
738+
expect(key.currentState, isNull);
739+
expect(state.didChangeDependenciesCount, 3);
740+
expect(state.deactivatedCount, 2);
741+
});
713742
}
714743

715744
class NullChildTest extends Widget {
@@ -755,3 +784,42 @@ class DirtyElementWithCustomBuildOwner extends Element {
755784
@override
756785
bool get dirty => true;
757786
}
787+
788+
class Inherited extends InheritedWidget {
789+
const Inherited(this.value, {Widget child, Key key}) : super(key: key, child: child);
790+
791+
final int value;
792+
793+
@override
794+
bool updateShouldNotify(Inherited oldWidget) => oldWidget.value != value;
795+
}
796+
797+
class DependentStatefulWidget extends StatefulWidget {
798+
const DependentStatefulWidget({Key key}) : super(key: key);
799+
800+
@override
801+
State<StatefulWidget> createState() => DependentState();
802+
}
803+
804+
class DependentState extends State<DependentStatefulWidget> {
805+
int didChangeDependenciesCount = 0;
806+
int deactivatedCount = 0;
807+
808+
@override
809+
void didChangeDependencies() {
810+
super.didChangeDependencies();
811+
didChangeDependenciesCount += 1;
812+
}
813+
814+
@override
815+
Widget build(BuildContext context) {
816+
context.dependOnInheritedWidgetOfExactType<Inherited>();
817+
return const SizedBox();
818+
}
819+
820+
@override
821+
void deactivate() {
822+
super.deactivate();
823+
deactivatedCount += 1;
824+
}
825+
}

0 commit comments

Comments
 (0)