Skip to content

Commit c75edc2

Browse files
authored
Dispose render objects when owning element is unmounted. (flutter#82883)
1 parent 8cf3924 commit c75edc2

File tree

12 files changed

+240
-12
lines changed

12 files changed

+240
-12
lines changed

examples/layers/rendering/src/sector_layout.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,6 @@ class RenderSectorSlice extends RenderSectorWithChildren {
455455
}
456456

457457
class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChildMixin<RenderSector> {
458-
459458
RenderBoxToRenderSectorAdapter({ double innerRadius = 0.0, RenderSector? child })
460459
: _innerRadius = innerRadius {
461460
this.child = child;
@@ -567,7 +566,6 @@ class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChil
567566
result.add(BoxHitTestEntry(this, position));
568567
return true;
569568
}
570-
571569
}
572570

573571
class RenderSolidColor extends RenderDecoratedSector {

examples/layers/widgets/sectors.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ class SectorAppState extends State<SectorApp> {
9090
});
9191
}
9292

93+
void recursivelyDisposeChildren(RenderObject parent) {
94+
parent.visitChildren((RenderObject child) {
95+
recursivelyDisposeChildren(child);
96+
child.dispose();
97+
});
98+
}
99+
93100
Widget buildBody() {
94101
return Column(
95102
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -107,7 +114,12 @@ class SectorAppState extends State<SectorApp> {
107114
Container(
108115
padding: const EdgeInsets.all(4.0),
109116
margin: const EdgeInsets.only(right: 10.0),
110-
child: WidgetToRenderBoxAdapter(renderBox: sectorAddIcon),
117+
child: WidgetToRenderBoxAdapter(
118+
renderBox: sectorAddIcon,
119+
onUnmount: () {
120+
recursivelyDisposeChildren(sectorAddIcon);
121+
},
122+
),
111123
),
112124
const Text('ADD SECTOR'),
113125
],
@@ -122,7 +134,12 @@ class SectorAppState extends State<SectorApp> {
122134
Container(
123135
padding: const EdgeInsets.all(4.0),
124136
margin: const EdgeInsets.only(right: 10.0),
125-
child: WidgetToRenderBoxAdapter(renderBox: sectorRemoveIcon),
137+
child: WidgetToRenderBoxAdapter(
138+
renderBox: sectorRemoveIcon,
139+
onUnmount: () {
140+
recursivelyDisposeChildren(sectorRemoveIcon);
141+
},
142+
),
126143
),
127144
const Text('REMOVE SECTOR'),
128145
],
@@ -142,6 +159,9 @@ class SectorAppState extends State<SectorApp> {
142159
child: WidgetToRenderBoxAdapter(
143160
renderBox: sectors,
144161
onBuild: doUpdates,
162+
onUnmount: () {
163+
recursivelyDisposeChildren(sectors);
164+
},
145165
),
146166
),
147167
),

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
283283
_RenderEditableCustomPaint? _foregroundRenderObject;
284284
_RenderEditableCustomPaint? _backgroundRenderObject;
285285

286+
@override
287+
void dispose() {
288+
_foregroundRenderObject?.dispose();
289+
_foregroundRenderObject = null;
290+
_backgroundRenderObject?.dispose();
291+
_backgroundRenderObject = null;
292+
super.dispose();
293+
}
294+
286295
void _updateForegroundPainter(RenderEditablePainter? newPainter) {
287296
final _CompositeRenderEditablePainter effectivePainter = newPainter == null
288297
? _builtInForegroundPainters

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,13 @@ class RenderImage extends RenderBox {
438438
);
439439
}
440440

441+
@override
442+
void dispose() {
443+
_image?.dispose();
444+
_image = null;
445+
super.dispose();
446+
}
447+
441448
@override
442449
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
443450
super.debugFillProperties(properties);

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,19 @@ class PipelineOwner {
11111111
/// The [RenderBox] subclass introduces the opinion that the layout
11121112
/// system uses Cartesian coordinates.
11131113
///
1114+
/// ## Lifecycle
1115+
///
1116+
/// A [RenderObject] must [dispose] when it is no longer needed. The creator
1117+
/// of the object is responsible for disposing of it. Typically, the creator is
1118+
/// a [RenderObjectElement], and that element will dispose the object it creates
1119+
/// when it is unmounted.
1120+
///
1121+
/// [RenderObject]s are responsible for cleaning up any expensive resources
1122+
/// they hold when [dispose] is called, such as [Picture] or [Image] objects.
1123+
/// This includes any [Layer]s that the render object has directly created. The
1124+
/// base implementation of dispose will nullify the [layer] property. Subclasses
1125+
/// must also nullify any other layer(s) it directly creates.
1126+
///
11141127
/// ## Writing a RenderObject subclass
11151128
///
11161129
/// In most cases, subclassing [RenderObject] itself is overkill, and
@@ -1230,6 +1243,50 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
12301243
});
12311244
}
12321245

1246+
/// Whether this has been disposed.
1247+
///
1248+
/// If asserts are disabled, this property is always null.
1249+
bool? get debugDisposed {
1250+
bool? disposed;
1251+
assert(() {
1252+
disposed = _debugDisposed;
1253+
return true;
1254+
}());
1255+
return disposed;
1256+
}
1257+
1258+
bool _debugDisposed = false;
1259+
1260+
/// Release any resources held by this render object.
1261+
///
1262+
/// The object that creates a RenderObject is in charge of disposing it.
1263+
/// If this render object has created any children directly, it must dispose
1264+
/// of those children in this method as well. It must not dispose of any
1265+
/// children that were created by some other object, such as
1266+
/// a [RenderObjectElement]. Those children will be disposed when that
1267+
/// element unmounts, which may be delayed if the element is moved to another
1268+
/// part of the tree.
1269+
///
1270+
/// Implementations of this method must end with a call to the inherited
1271+
/// method, as in `super.dispose()`.
1272+
///
1273+
/// The object is no longer usable after calling dispose.
1274+
@mustCallSuper
1275+
void dispose() {
1276+
assert(!_debugDisposed);
1277+
_layer = null;
1278+
assert(() {
1279+
visitChildren((RenderObject child) {
1280+
assert(
1281+
child.debugDisposed!,
1282+
'${child.runtimeType} (child of $runtimeType) must be disposed before calling super.dispose().',
1283+
);
1284+
});
1285+
_debugDisposed = true;
1286+
return true;
1287+
}());
1288+
}
1289+
12331290
// LAYOUT
12341291

12351292
/// Data for use by the parent render object.
@@ -1367,6 +1424,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
13671424
bool get _debugCanPerformMutations {
13681425
late bool result;
13691426
assert(() {
1427+
if (_debugDisposed) {
1428+
result = false;
1429+
return true;
1430+
}
13701431
if (owner != null && !owner!.debugDoingLayout) {
13711432
result = true;
13721433
return true;
@@ -1401,6 +1462,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
14011462

14021463
@override
14031464
void attach(PipelineOwner owner) {
1465+
assert(!_debugDisposed);
14041466
super.attach(owner);
14051467
// If the node was dirtied in some way while unattached, make sure to add
14061468
// it to the appropriate dirty list now that an owner is available
@@ -1612,6 +1674,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
16121674
///
16131675
/// See [RenderView] for an example of how this function is used.
16141676
void scheduleInitialLayout() {
1677+
assert(!_debugDisposed);
16151678
assert(attached);
16161679
assert(parent is! RenderObject);
16171680
assert(!owner!._debugDoingLayout);
@@ -1681,6 +1744,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
16811744
/// work to update its layout information.
16821745
@pragma('vm:notify-debugger-on-exception')
16831746
void layout(Constraints constraints, { bool parentUsesSize = false }) {
1747+
assert(!_debugDisposed);
16841748
if (!kReleaseMode && debugProfileLayoutsEnabled)
16851749
Timeline.startSync('$runtimeType', arguments: timelineArgumentsIndicatingLandmarkEvent);
16861750

@@ -2040,6 +2104,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
20402104
/// it cannot be the case that _only_ the compositing bits changed,
20412105
/// something else will have scheduled a frame for us.
20422106
void markNeedsCompositingBitsUpdate() {
2107+
assert(!_debugDisposed);
20432108
if (_needsCompositingBitsUpdate)
20442109
return;
20452110
_needsCompositingBitsUpdate = true;
@@ -2138,6 +2203,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
21382203
/// layer, thus limiting the number of nodes that [markNeedsPaint] must mark
21392204
/// dirty.
21402205
void markNeedsPaint() {
2206+
assert(!_debugDisposed);
21412207
assert(owner == null || !owner!.debugDoingPaint);
21422208
if (_needsPaint)
21432209
return;
@@ -2222,6 +2288,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
22222288
///
22232289
/// This might be called if, e.g., the device pixel ratio changed.
22242290
void replaceRootLayer(OffsetLayer rootLayer) {
2291+
assert(!_debugDisposed);
22252292
assert(rootLayer.attached);
22262293
assert(attached);
22272294
assert(parent is! RenderObject);
@@ -2234,6 +2301,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
22342301
}
22352302

22362303
void _paintWithContext(PaintingContext context, Offset offset) {
2304+
assert(!_debugDisposed);
22372305
assert(() {
22382306
if (_debugDoingThisPaint) {
22392307
throw FlutterError.fromParts(<DiagnosticsNode>[
@@ -2457,6 +2525,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
24572525
///
24582526
/// See [RendererBinding] for an example of how this function is used.
24592527
void scheduleInitialSemantics() {
2528+
assert(!_debugDisposed);
24602529
assert(attached);
24612530
assert(parent is! RenderObject);
24622531
assert(!owner!._debugDoingSemantics);
@@ -2579,6 +2648,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
25792648
/// [RenderObject] as annotated by [describeSemanticsConfiguration] changes in
25802649
/// any way to update the semantics tree.
25812650
void markNeedsSemanticsUpdate() {
2651+
assert(!_debugDisposed);
25822652
assert(!attached || !owner!._debugDoingSemantics);
25832653
if (!attached || owner!._semanticsOwner == null) {
25842654
_cachedSemanticsConfiguration = null;
@@ -2824,6 +2894,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
28242894
@override
28252895
String toStringShort() {
28262896
String header = describeIdentity(this);
2897+
if (_debugDisposed) {
2898+
header += ' DISPOSED';
2899+
return header;
2900+
}
28272901
if (_relayoutBoundary != null && _relayoutBoundary != this) {
28282902
int count = 1;
28292903
RenderObject? target = parent as RenderObject?;
@@ -3209,6 +3283,7 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
32093283
}
32103284
}
32113285
}
3286+
32123287
/// Insert child into this render object's child list after the given child.
32133288
///
32143289
/// If `after` is null, then this inserts the child at the start of the list,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3152,7 +3152,6 @@ class RenderRepaintBoundary extends RenderProxyBox {
31523152
return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
31533153
}
31543154

3155-
31563155
/// The number of times that this render object repainted at the same time as
31573156
/// its parent. Repaint boundaries are only useful when the parent and child
31583157
/// paint at different times. When both paint at the same time, the repaint

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6232,13 +6232,19 @@ class DefaultAssetBundle extends InheritedWidget {
62326232
/// A given render object can be placed at most once in the widget tree. This
62336233
/// widget enforces that restriction by keying itself using a [GlobalObjectKey]
62346234
/// for the given render object.
6235+
///
6236+
/// This widget will call [RenderObject.dispose] on the [renderBox] when it is
6237+
/// unmounted. After that point, the [renderBox] will be unusable. If any
6238+
/// children have been added to the [renderBox], they must be disposed in the
6239+
/// [onUnmount] callback.
62356240
class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
62366241
/// Creates an adapter for placing a specific [RenderBox] in the widget tree.
62376242
///
62386243
/// The [renderBox] argument must not be null.
62396244
WidgetToRenderBoxAdapter({
62406245
required this.renderBox,
62416246
this.onBuild,
6247+
this.onUnmount,
62426248
}) : assert(renderBox != null),
62436249
// WidgetToRenderBoxAdapter objects are keyed to their render box. This
62446250
// prevents the widget being used in the widget hierarchy in two different
@@ -6247,6 +6253,9 @@ class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
62476253
super(key: GlobalObjectKey(renderBox));
62486254

62496255
/// The render box to place in the widget tree.
6256+
///
6257+
/// This widget takes ownership of the render object. When it is unmounted,
6258+
/// it also calls [RenderObject.dispose].
62506259
final RenderBox renderBox;
62516260

62526261
/// Called when it is safe to update the render box and its descendants. If
@@ -6255,13 +6264,38 @@ class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
62556264
/// tree will be dirty.
62566265
final VoidCallback? onBuild;
62576266

6267+
/// Called when it is safe to dispose of children that were manually added to
6268+
/// the [renderBox].
6269+
///
6270+
/// Do not dispose the [renderBox] itself, as it will be disposed by the
6271+
/// framework automatically. However, during that process the framework will
6272+
/// check that all children of the [renderBox] have also been disposed.
6273+
/// Typically, child [RenderObject]s are disposed by corresponding [Element]s
6274+
/// when they are unmounted. However, child render objects that were manually
6275+
/// added do not have corresponding [Element]s to manage their lifecycle, and
6276+
/// need to be manually disposed here.
6277+
///
6278+
/// See also:
6279+
///
6280+
/// * [RenderObjectElement.unmount], which invokes this callback before
6281+
/// disposing of its render object.
6282+
/// * [RenderObject.dispose], which instructs a render object to release
6283+
/// any resources it may be holding.
6284+
final VoidCallback? onUnmount;
6285+
62586286
@override
62596287
RenderBox createRenderObject(BuildContext context) => renderBox;
62606288

62616289
@override
62626290
void updateRenderObject(BuildContext context, RenderBox renderObject) {
62636291
onBuild?.call();
62646292
}
6293+
6294+
@override
6295+
void didUnmountRenderObject(RenderObject renderObject) {
6296+
assert(renderObject == renderBox);
6297+
onUnmount?.call();
6298+
}
62656299
}
62666300

62676301

0 commit comments

Comments
 (0)