Skip to content

Commit 2409e7d

Browse files
Hixiegspencergoog
authored andcommitted
Keep-alive for widgets in lazy lists (flutter#11010)
1 parent ffb76ba commit 2409e7d

File tree

10 files changed

+905
-61
lines changed

10 files changed

+905
-61
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
130130
final int oldLastIndex = indexOf(lastChild);
131131
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
132132
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
133-
if (leadingGarbage + trailingGarbage > 0)
134-
collectGarbage(leadingGarbage, trailingGarbage);
133+
collectGarbage(leadingGarbage, trailingGarbage);
134+
} else {
135+
collectGarbage(0, 0);
135136
}
136137

137138
if (firstChild == null) {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
507507
final int oldLastIndex = indexOf(lastChild);
508508
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
509509
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
510-
if (leadingGarbage + trailingGarbage > 0)
511-
collectGarbage(leadingGarbage, trailingGarbage);
510+
collectGarbage(leadingGarbage, trailingGarbage);
511+
} else {
512+
collectGarbage(0, 0);
512513
}
513514

514515
final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex);

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

Lines changed: 179 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,15 @@ class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with Conta
111111
/// The index of this child according to the [RenderSliverBoxChildManager].
112112
int index;
113113

114+
/// Whether to keep the child alive even when it is no longer visible.
115+
bool keepAlive = false;
116+
117+
/// Whether the widget is currently in the
118+
/// [RenderSliverMultiBoxAdaptor._keepAliveBucket].
119+
bool _keptAlive = false;
120+
114121
@override
115-
String toString() => 'index=$index; ${super.toString()}';
122+
String toString() => 'index=$index; ${keepAlive == true ? "keepAlive; " : ""}${super.toString()}';
116123
}
117124

118125
/// A sliver with multiple box children.
@@ -168,10 +175,15 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
168175
RenderSliverBoxChildManager get childManager => _childManager;
169176
final RenderSliverBoxChildManager _childManager;
170177

178+
/// The nodes being kept alive despite not being visible.
179+
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
180+
171181
@override
172182
void adoptChild(RenderObject child) {
173183
super.adoptChild(child);
174-
childManager.didAdoptChild(child);
184+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
185+
if (!childParentData._keptAlive)
186+
childManager.didAdoptChild(child);
175187
}
176188

177189
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
@@ -192,64 +204,139 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
192204
});
193205
}
194206

207+
@override
208+
void remove(RenderBox child) {
209+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
210+
if (!childParentData._keptAlive) {
211+
super.remove(child);
212+
return;
213+
}
214+
assert(_keepAliveBucket[childParentData.index] == child);
215+
_keepAliveBucket.remove(childParentData.index);
216+
dropChild(child);
217+
}
218+
219+
@override
220+
void removeAll() {
221+
super.removeAll();
222+
for (RenderBox child in _keepAliveBucket.values)
223+
dropChild(child);
224+
_keepAliveBucket.clear();
225+
}
226+
227+
void _createOrObtainChild(int index, { RenderBox after }) {
228+
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
229+
assert(constraints == this.constraints);
230+
if (_keepAliveBucket.containsKey(index)) {
231+
final RenderBox child = _keepAliveBucket.remove(index);
232+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
233+
assert(childParentData._keptAlive);
234+
dropChild(child);
235+
child.parentData = childParentData;
236+
insert(child, after: after);
237+
childParentData._keptAlive = false;
238+
} else {
239+
_childManager.createChild(index, after: after);
240+
}
241+
});
242+
}
243+
244+
void _destroyOrCacheChild(RenderBox child) {
245+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
246+
if (childParentData.keepAlive) {
247+
assert(!childParentData._keptAlive);
248+
remove(child);
249+
_keepAliveBucket[childParentData.index] = child;
250+
child.parentData = childParentData;
251+
super.adoptChild(child);
252+
childParentData._keptAlive = true;
253+
} else {
254+
assert(child.parent == this);
255+
_childManager.removeChild(child);
256+
assert(child.parent == null);
257+
}
258+
}
259+
260+
@override
261+
void attach(PipelineOwner owner) {
262+
super.attach(owner);
263+
for (RenderBox child in _keepAliveBucket.values)
264+
child.attach(owner);
265+
}
266+
267+
@override
268+
void detach() {
269+
super.detach();
270+
for (RenderBox child in _keepAliveBucket.values)
271+
child.detach();
272+
}
273+
274+
@override
275+
void redepthChildren() {
276+
super.redepthChildren();
277+
for (RenderBox child in _keepAliveBucket.values)
278+
redepthChild(child);
279+
}
280+
281+
@override
282+
void visitChildren(RenderObjectVisitor visitor) {
283+
super.visitChildren(visitor);
284+
for (RenderBox child in _keepAliveBucket.values)
285+
visitor(child);
286+
}
287+
195288
/// Called during layout to create and add the child with the given index and
196289
/// scroll offset.
197290
///
198291
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
199-
/// the child.
292+
/// the child if necessary. The child may instead be obtained from a cache;
293+
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
200294
///
201-
/// Returns false if createChild did not add any child, otherwise returns
202-
/// true.
295+
/// Returns false if there was no cached child and `createChild` did not add
296+
/// any child, otherwise returns true.
203297
///
204298
/// Does not layout the new child.
205299
///
206-
/// When this is called, there are no children, so no children can be removed
207-
/// during the call to createChild. No child should be added during that call
208-
/// either, except for the one that is created and returned by createChild.
300+
/// When this is called, there are no visible children, so no children can be
301+
/// removed during the call to `createChild`. No child should be added during
302+
/// that call either, except for the one that is created and returned by
303+
/// `createChild`.
209304
@protected
210305
bool addInitialChild({ int index: 0, double layoutOffset: 0.0 }) {
211306
assert(_debugAssertChildListLocked());
212307
assert(firstChild == null);
213-
bool result;
214-
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
215-
assert(constraints == this.constraints);
216-
_childManager.createChild(index, after: null);
217-
if (firstChild != null) {
218-
assert(firstChild == lastChild);
219-
assert(indexOf(firstChild) == index);
220-
final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
221-
firstChildParentData.layoutOffset = layoutOffset;
222-
result = true;
223-
} else {
224-
childManager.setDidUnderflow(true);
225-
result = false;
226-
}
227-
});
228-
return result;
308+
_createOrObtainChild(index, after: null);
309+
if (firstChild != null) {
310+
assert(firstChild == lastChild);
311+
assert(indexOf(firstChild) == index);
312+
final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
313+
firstChildParentData.layoutOffset = layoutOffset;
314+
return true;
315+
}
316+
childManager.setDidUnderflow(true);
317+
return false;
229318
}
230319

231320
/// Called during layout to create, add, and layout the child before
232321
/// [firstChild].
233322
///
234323
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
235-
/// the child.
324+
/// the child if necessary. The child may instead be obtained from a cache;
325+
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
236326
///
237-
/// Returns the new child or null if no child is created.
327+
/// Returns the new child or null if no child was obtained.
238328
///
239329
/// The child that was previously the first child, as well as any subsequent
240330
/// children, may be removed by this call if they have not yet been laid out
241331
/// during this layout pass. No child should be added during that call except
242-
/// for the one that is created and returned by createChild.
332+
/// for the one that is created and returned by `createChild`.
243333
@protected
244334
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {
245335
bool parentUsesSize: false,
246336
}) {
247337
assert(_debugAssertChildListLocked());
248338
final int index = indexOf(firstChild) - 1;
249-
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
250-
assert(constraints == this.constraints);
251-
_childManager.createChild(index, after: null);
252-
});
339+
_createOrObtainChild(index, after: null);
253340
if (indexOf(firstChild) == index) {
254341
firstChild.layout(childConstraints, parentUsesSize: parentUsesSize);
255342
return firstChild;
@@ -262,7 +349,8 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
262349
/// the given child.
263350
///
264351
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
265-
/// the child.
352+
/// the child if necessary. The child may instead be obtained from a cache;
353+
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
266354
///
267355
/// Returns the new child. It is the responsibility of the caller to configure
268356
/// the child's scroll offset.
@@ -277,13 +365,9 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
277365
assert(_debugAssertChildListLocked());
278366
assert(after != null);
279367
final int index = indexOf(after) + 1;
280-
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
281-
assert(constraints == this.constraints);
282-
_childManager.createChild(index, after: after);
283-
});
368+
_createOrObtainChild(index, after: after);
284369
final RenderBox child = childAfter(after);
285370
if (child != null && indexOf(child) == index) {
286-
assert(indexOf(child) == index);
287371
child.layout(childConstraints, parentUsesSize: parentUsesSize);
288372
return child;
289373
}
@@ -293,19 +377,37 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
293377

294378
/// Called after layout with the number of children that can be garbage
295379
/// collected at the head and tail of the child list.
380+
///
381+
/// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is
382+
/// set to true will be removed to a cache instead of being dropped.
383+
///
384+
/// This method also collects any children that were previously kept alive but
385+
/// are now no longer necessary. As such, it should be called every time
386+
/// [performLayout] is run, even if the arguments are both zero.
296387
@protected
297388
void collectGarbage(int leadingGarbage, int trailingGarbage) {
298389
assert(_debugAssertChildListLocked());
299390
assert(childCount >= leadingGarbage + trailingGarbage);
300391
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
301392
while (leadingGarbage > 0) {
302-
_childManager.removeChild(firstChild);
393+
_destroyOrCacheChild(firstChild);
303394
leadingGarbage -= 1;
304395
}
305396
while (trailingGarbage > 0) {
306-
_childManager.removeChild(lastChild);
397+
_destroyOrCacheChild(lastChild);
307398
trailingGarbage -= 1;
308399
}
400+
// Ask the child manager to remove the children that are no longer being
401+
// kept alive. (This should cause _keepAliveBucket to change, so we have
402+
// to prepare our list ahead of time.)
403+
_keepAliveBucket.values.where((RenderBox child) {
404+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
405+
return !childParentData.keepAlive;
406+
}).toList().forEach(_childManager.removeChild);
407+
assert(_keepAliveBucket.values.where((RenderBox child) {
408+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
409+
return !childParentData.keepAlive;
410+
}).isEmpty);
309411
});
310412
}
311413

@@ -442,4 +544,42 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
442544
});
443545
return true;
444546
}
547+
548+
@override
549+
String debugDescribeChildren(String prefix) {
550+
StringBuffer result;
551+
if (firstChild != null) {
552+
result = new StringBuffer()
553+
..write(prefix)
554+
..write(' \u2502\n');
555+
RenderBox child = firstChild;
556+
while (child != lastChild) {
557+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
558+
result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u2502"));
559+
child = childParentData.nextSibling;
560+
}
561+
if (child != null) {
562+
assert(child == lastChild);
563+
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
564+
if (_keepAliveBucket.isEmpty) {
565+
result.write(child.toStringDeep("$prefix \u2514\u2500child with index ${childParentData.index}: ", "$prefix "));
566+
} else {
567+
result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u254E"));
568+
}
569+
}
570+
}
571+
if (_keepAliveBucket.isNotEmpty) {
572+
result ??= new StringBuffer()
573+
..write(prefix)
574+
..write(' \u254E\n');
575+
final List<int> indices = _keepAliveBucket.keys.toList()..sort();
576+
final int lastIndex = indices.removeLast();
577+
if (indices.isNotEmpty) {
578+
for (int index in indices)
579+
result.write(_keepAliveBucket[index].toStringDeep("$prefix \u251C\u2500child with index $index (kept alive offstage): ", "$prefix \u254E"));
580+
}
581+
result.write(_keepAliveBucket[lastIndex].toStringDeep("$prefix \u2514\u2500child with index $lastIndex (kept alive offstage): ", "$prefix "));
582+
}
583+
return result?.toString() ?? '';
584+
}
445585
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,8 +1201,9 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
12011201

12021202
/// Meta data for identifying children in a [CustomMultiChildLayout].
12031203
///
1204-
/// The [MultiChildLayoutDelegate] hasChild, layoutChild, and positionChild
1205-
/// methods use these identifiers.
1204+
/// The [MultiChildLayoutDelegate.hasChild],
1205+
/// [MultiChildLayoutDelegate.layoutChild], and
1206+
/// [MultiChildLayoutDelegate.positionChild] methods use these identifiers.
12061207
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
12071208
/// Marks a child with a layout identifier.
12081209
///

0 commit comments

Comments
 (0)