Skip to content

Commit 2fcda04

Browse files
Add error handling for Element lifecycle user callbacks (flutter#173148)
This is for flutter#172289, but since this fix is speculative so I'll wait for the confirmation from the original reporters before closing the issue. As a bonus this fixes flutter#65655 The framework Element rebuild logic relies heavily on `Element._lifecycleState` being correct. When a user-supplied lifecycle callback (e.g., `State.deactivate`) fails the framework currently only ensures that every `Element` in the tree has the right lifecycle state, so an out-of-tree `Element` that is supposed to be disposed may still have an `active` state and continue being rebuilt by the BuildScope (because it's in the dirty list). See the comments in flutter#172289 Also related: flutter#100777 Internal: b/425298525 b/431537277 b/300829376 b/415724119 b/283614822 # TODO (in a different PR) The original issue could also be caused by incorrect `Element.updateChild` calls. If an `Element` subclass calls `Element.updateChild` to add child but forgets to update its child list accordingly (such that `visitChildren` misses that child), you'll get a child Element that thinks it's a child of the parent but the parent doesn't recognize the child so won't take that child into account during reparenting or unmounting. This is a programmer error that we should try to catch in the framework. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] 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. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **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 [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
1 parent ad7ee1c commit 2fcda04

File tree

9 files changed

+242
-91
lines changed

9 files changed

+242
-91
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2059,7 +2059,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
20592059
assert(child._parent == this);
20602060
assert(child.attached == attached);
20612061
assert(child.parentData != null);
2062-
if (!(_isRelayoutBoundary ?? true)) {
2062+
if (!(child._isRelayoutBoundary ?? true)) {
20632063
child._isRelayoutBoundary = null;
20642064
}
20652065
child.parentData!.detach();

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

Lines changed: 146 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,13 +2053,54 @@ abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
20532053

20542054
// ELEMENTS
20552055

2056-
enum _ElementLifecycle { initial, active, inactive, defunct }
2056+
enum _ElementLifecycle {
2057+
/// The [Element] is created but has not yet been incorporated into the element
2058+
/// tree.
2059+
initial,
2060+
2061+
/// The [Element] is incorporated into the Element tree, either via
2062+
/// [Element.mount] or [Element.activate].
2063+
active,
2064+
2065+
/// The previously `active` [Element] is removed from the Element tree via
2066+
/// [Element.deactivate].
2067+
///
2068+
/// This [Element] may become `active` again if a parent reclaims it using
2069+
/// a [GlobalKey], or `defunct` if no parent reclaims it at the end of the
2070+
/// build phase.
2071+
inactive,
2072+
2073+
/// The [Element] encountered an unrecoverable error while being rebuilt when it
2074+
/// was `active` or while being incorporated in the tree.
2075+
///
2076+
/// This indicates the [Element]'s subtree is in an inconsistent state and must
2077+
/// not be re-incorporated into the tree again.
2078+
///
2079+
/// When an unrecoverable error is encountered, the framework calls
2080+
/// [Element.deactivate] on this [Element] and sets its state to `failed`. This
2081+
/// process is done on a best-effort basis and does not surface any additional
2082+
/// errors.
2083+
///
2084+
/// This is one of the two final stages of the element lifecycle and is not
2085+
/// reversible. Reaching this state typically means that a widget implementation
2086+
/// is throwing unhandled exceptions that need to be properly handled.
2087+
failed,
2088+
2089+
/// The [Element] is disposed and should not be interacted with.
2090+
///
2091+
/// The [Element] must be `inactive` before transitioning into this state,
2092+
/// and the state transition occurs in [BuildOwner.finalizeTree] which signals
2093+
/// the end of the build phase.
2094+
///
2095+
/// This is the final stage of the element lifecycle and is not reversible.
2096+
defunct,
2097+
}
20572098

20582099
class _InactiveElements {
20592100
bool _locked = false;
20602101
final Set<Element> _elements = HashSet<Element>();
20612102

2062-
void _unmount(Element element) {
2103+
static void _unmount(Element element) {
20632104
assert(element._lifecycleState == _ElementLifecycle.inactive);
20642105
assert(() {
20652106
if (debugPrintGlobalKeyedWidgetLifecycle) {
@@ -2091,8 +2132,12 @@ class _InactiveElements {
20912132

20922133
static void _deactivateRecursively(Element element) {
20932134
assert(element._lifecycleState == _ElementLifecycle.active);
2094-
element.deactivate();
2095-
assert(element._lifecycleState == _ElementLifecycle.inactive);
2135+
try {
2136+
element.deactivate();
2137+
} catch (_) {
2138+
Element._deactivateFailedSubtreeRecursively(element);
2139+
rethrow;
2140+
}
20962141
element.visitChildren(_deactivateRecursively);
20972142
assert(() {
20982143
element.debugDeactivated();
@@ -2104,18 +2149,26 @@ class _InactiveElements {
21042149
assert(!_locked);
21052150
assert(!_elements.contains(element));
21062151
assert(element._parent == null);
2107-
if (element._lifecycleState == _ElementLifecycle.active) {
2108-
_deactivateRecursively(element);
2152+
2153+
switch (element._lifecycleState) {
2154+
case _ElementLifecycle.active:
2155+
_deactivateRecursively(element);
2156+
// This element is only added to _elements if the whole subtree is
2157+
// successfully deactivated.
2158+
_elements.add(element);
2159+
case _ElementLifecycle.inactive:
2160+
_elements.add(element);
2161+
case _ElementLifecycle.initial || _ElementLifecycle.failed || _ElementLifecycle.defunct:
2162+
assert(false, '$element must not be deactivated when in ${element._lifecycleState} state.');
21092163
}
2110-
_elements.add(element);
21112164
}
21122165

21132166
void remove(Element element) {
21142167
assert(!_locked);
21152168
assert(_elements.contains(element));
21162169
assert(element._parent == null);
21172170
_elements.remove(element);
2118-
assert(element._lifecycleState != _ElementLifecycle.active);
2171+
assert(element._lifecycleState == _ElementLifecycle.inactive);
21192172
}
21202173

21212174
bool debugContains(Element element) {
@@ -2934,7 +2987,7 @@ class BuildOwner {
29342987
buildScope._scheduleBuildFor(element);
29352988
assert(() {
29362989
if (debugPrintScheduleBuildForStacks) {
2937-
debugPrint("...the build scope's dirty list is now: $buildScope._dirtyElements");
2990+
debugPrint("...the build scope's dirty list is now: ${buildScope._dirtyElements}");
29382991
}
29392992
return true;
29402993
}());
@@ -4516,39 +4569,32 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
45164569

45174570
try {
45184571
final Key? key = newWidget.key;
4519-
if (key is GlobalKey) {
4520-
final Element? newChild = _retakeInactiveElement(key, newWidget);
4521-
if (newChild != null) {
4522-
assert(newChild._parent == null);
4523-
assert(() {
4524-
_debugCheckForCycles(newChild);
4525-
return true;
4526-
}());
4527-
try {
4528-
newChild._activateWithParent(this, newSlot);
4529-
} catch (_) {
4530-
// Attempt to do some clean-up if activation fails to leave tree in a reasonable state.
4531-
try {
4532-
deactivateChild(newChild);
4533-
} catch (_) {
4534-
// Clean-up failed. Only surface original exception.
4535-
}
4536-
rethrow;
4537-
}
4538-
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
4539-
assert(newChild == updatedChild);
4540-
return updatedChild!;
4541-
}
4542-
}
4543-
final Element newChild = newWidget.createElement();
4572+
final Element? inactiveChild = key is GlobalKey
4573+
? _retakeInactiveElement(key, newWidget)
4574+
: null;
4575+
final Element newChild = inactiveChild ?? newWidget.createElement();
45444576
assert(() {
45454577
_debugCheckForCycles(newChild);
45464578
return true;
45474579
}());
4548-
newChild.mount(this, newSlot);
4549-
assert(newChild._lifecycleState == _ElementLifecycle.active);
4550-
4551-
return newChild;
4580+
try {
4581+
if (inactiveChild != null) {
4582+
assert(inactiveChild._parent == null);
4583+
inactiveChild._activateWithParent(this, newSlot);
4584+
final Element? updatedChild = updateChild(inactiveChild, newWidget, newSlot);
4585+
assert(inactiveChild == updatedChild);
4586+
return updatedChild!;
4587+
} else {
4588+
newChild.mount(this, newSlot);
4589+
assert(newChild._lifecycleState == _ElementLifecycle.active);
4590+
return newChild;
4591+
}
4592+
} catch (_) {
4593+
// Attempt to do some clean-up if activation or mount fails
4594+
// to leave tree in a reasonable state.
4595+
_deactivateFailedChildSilently(newChild);
4596+
rethrow;
4597+
}
45524598
} finally {
45534599
if (isTimelineTracked) {
45544600
FlutterTimeline.finishSync();
@@ -4583,6 +4629,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
45834629
/// parent proactively calls the old parent's [deactivateChild], first using
45844630
/// [forgetChild] to cause the old parent to update its child model.
45854631
@protected
4632+
@mustCallSuper
45864633
void deactivateChild(Element child) {
45874634
assert(child._parent == this);
45884635
child._parent = null;
@@ -4598,6 +4645,38 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
45984645
}());
45994646
}
46004647

4648+
void _deactivateFailedChildSilently(Element child) {
4649+
try {
4650+
child._parent = null;
4651+
child.detachRenderObject();
4652+
_deactivateFailedSubtreeRecursively(child);
4653+
} catch (_) {
4654+
// Do not rethrow:
4655+
// The subtree has already thrown a different error and the framework is
4656+
// cleaning up on a best-effort basis.
4657+
}
4658+
}
4659+
4660+
// This method calls _ensureDeactivated for the subtree rooted at `element`,
4661+
// supressing all exceptions thrown.
4662+
//
4663+
// This method will attempt to keep doing treewalk even one of the nodes
4664+
// failed to deactivate.
4665+
//
4666+
// The subtree has already thrown a different error and the framework is
4667+
// cleaning up on a best-effort basis.
4668+
static void _deactivateFailedSubtreeRecursively(Element element) {
4669+
try {
4670+
element.deactivate();
4671+
} catch (_) {
4672+
element._ensureDeactivated();
4673+
}
4674+
element._lifecycleState = _ElementLifecycle.failed;
4675+
try {
4676+
element.visitChildren(_deactivateFailedSubtreeRecursively);
4677+
} catch (_) {}
4678+
}
4679+
46014680
// The children that have been forgotten by forgetChild. This will be used in
46024681
// [update] to remove the global key reservations of forgotten children.
46034682
//
@@ -4672,6 +4751,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
46724751
/// Implementations of this method should start with a call to the inherited
46734752
/// method, as in `super.activate()`.
46744753
@mustCallSuper
4754+
@visibleForOverriding
46754755
void activate() {
46764756
assert(_lifecycleState == _ElementLifecycle.inactive);
46774757
assert(owner != null);
@@ -4701,18 +4781,34 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
47014781
/// animation frame, if the element has not be reactivated, the framework will
47024782
/// unmount the element.
47034783
///
4704-
/// This is (indirectly) called by [deactivateChild].
4784+
/// In case of an uncaught exception when rebuild a widget subtree, the
4785+
/// framework also calls this method on the failing subtree to make sure the
4786+
/// widget tree is in a relatively consistent state. The deactivation of such
4787+
/// subtrees are performed only on a best-effort basis, and the errors thrown
4788+
/// during deactivation will not be rethrown.
4789+
///
4790+
/// This is indirectly called by [deactivateChild].
47054791
///
47064792
/// See the lifecycle documentation for [Element] for additional information.
47074793
///
47084794
/// Implementations of this method should end with a call to the inherited
47094795
/// method, as in `super.deactivate()`.
47104796
@mustCallSuper
4797+
@visibleForOverriding
47114798
void deactivate() {
47124799
assert(_lifecycleState == _ElementLifecycle.active);
47134800
assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
4714-
if (_dependencies?.isNotEmpty ?? false) {
4715-
for (final InheritedElement dependency in _dependencies!) {
4801+
_ensureDeactivated();
4802+
}
4803+
4804+
/// Removes dependencies and sets the lifecycle state of this [Element] to
4805+
/// inactive.
4806+
///
4807+
/// This method is immediately called after [Element.deactivate], even if that
4808+
/// call throws an exception.
4809+
void _ensureDeactivated() {
4810+
if (_dependencies case final Set<InheritedElement> dependencies? when dependencies.isNotEmpty) {
4811+
for (final InheritedElement dependency in dependencies) {
47164812
dependency.removeDependent(this);
47174813
}
47184814
// For expediency, we don't actually clear the list here, even though it's
@@ -4977,8 +5073,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
49775073

49785074
@override
49795075
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {Object? aspect}) {
4980-
_dependencies ??= HashSet<InheritedElement>();
4981-
_dependencies!.add(ancestor);
5076+
(_dependencies ??= HashSet<InheritedElement>()).add(ancestor);
49825077
ancestor.updateDependencies(this, aspect);
49835078
return ancestor.widget as InheritedWidget;
49845079
}
@@ -5714,7 +5809,7 @@ abstract class ComponentElement extends Element {
57145809
@override
57155810
@pragma('vm:notify-debugger-on-exception')
57165811
void performRebuild() {
5717-
Widget? built;
5812+
Widget built;
57185813
try {
57195814
assert(() {
57205815
_debugDoingBuild = true;
@@ -5757,6 +5852,10 @@ abstract class ComponentElement extends Element {
57575852
],
57585853
),
57595854
);
5855+
// _Make sure the old child subtree are deactivated and disposed.
5856+
try {
5857+
_child?.deactivate();
5858+
} catch (_) {}
57605859
_child = updateChild(null, built, slot);
57615860
}
57625861
}
@@ -6167,10 +6266,7 @@ class InheritedElement extends ProxyElement {
61676266

61686267
@override
61696268
void debugDeactivated() {
6170-
assert(() {
6171-
assert(_dependents.isEmpty);
6172-
return true;
6173-
}());
6269+
assert(_dependents.isEmpty);
61746270
super.debugDeactivated();
61756271
}
61766272

@@ -6748,6 +6844,7 @@ abstract class RenderObjectElement extends Element {
67486844
}
67496845

67506846
@override
6847+
@visibleForOverriding
67516848
void deactivate() {
67526849
super.deactivate();
67536850
assert(
@@ -6758,6 +6855,7 @@ abstract class RenderObjectElement extends Element {
67586855
}
67596856

67606857
@override
6858+
@visibleForOverriding
67616859
void unmount() {
67626860
assert(
67636861
!renderObject.debugDisposed!,

packages/flutter/test/cupertino/tab_test.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ void main() {
9595

9696
expect(tester.takeException(), isFlutterError);
9797
expect(unknownForRouteCalled, '/');
98-
99-
// Work-around for https://github.com/flutter/flutter/issues/65655.
100-
await tester.pumpWidget(Container());
101-
expect(tester.takeException(), isAssertionError);
10298
},
10399
);
104100

packages/flutter/test/material/app_test.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,6 @@ void main() {
347347
);
348348
expect(tester.takeException(), isFlutterError);
349349
expect(log, <String>['onGenerateRoute /', 'onUnknownRoute /']);
350-
351-
// Work-around for https://github.com/flutter/flutter/issues/65655.
352-
await tester.pumpWidget(Container());
353-
expect(tester.takeException(), isAssertionError);
354350
},
355351
);
356352

0 commit comments

Comments
 (0)