Fixes RenderSliverFixedExtentBoxAdaptor correctly calculates leadingGarbage and trailingGarbage. (#36302)
diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
index 221897f..131cf38 100644
--- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
+++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
@@ -142,6 +142,26 @@
return childManager.childCount * itemExtent;
}
+ int _calculateLeadingGarbage(int firstIndex) {
+ RenderBox walker = firstChild;
+ int leadingGarbage = 0;
+ while(walker != null && indexOf(walker) < firstIndex){
+ leadingGarbage += 1;
+ walker = childAfter(walker);
+ }
+ return leadingGarbage;
+ }
+
+ int _calculateTrailingGarbage(int targetLastIndex) {
+ RenderBox walker = lastChild;
+ int trailingGarbage = 0;
+ while(walker != null && indexOf(walker) > targetLastIndex){
+ trailingGarbage += 1;
+ walker = childBefore(walker);
+ }
+ return trailingGarbage;
+ }
+
@override
void performLayout() {
childManager.didStartLayout();
@@ -165,10 +185,8 @@
getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemExtent) : null;
if (firstChild != null) {
- final int oldFirstIndex = indexOf(firstChild);
- final int oldLastIndex = indexOf(lastChild);
- final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
- final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
+ final int leadingGarbage = _calculateLeadingGarbage(firstIndex);
+ final int trailingGarbage = _calculateTrailingGarbage(targetLastIndex);
collectGarbage(leadingGarbage, trailingGarbage);
} else {
collectGarbage(0, 0);
diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart
index e2c4c93..44a942b 100644
--- a/packages/flutter/test/widgets/slivers_test.dart
+++ b/packages/flutter/test/widgets/slivers_test.dart
@@ -25,6 +25,37 @@
);
}
+Future<void> testSliverFixedExtentList(WidgetTester tester, List<String> items) {
+ return tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CustomScrollView(
+ slivers: <Widget>[
+ SliverFixedExtentList(
+ itemExtent: 900,
+ delegate: SliverChildBuilderDelegate(
+ (BuildContext context, int index) {
+ return Center(
+ key: ValueKey<String>(items[index]),
+ child: KeepAlive(
+ items[index],
+ )
+ );
+ },
+ childCount : items.length,
+ findChildIndexCallback: (Key key) {
+ final ValueKey<String> valueKey = key;
+ final String data = valueKey.value;
+ return items.indexOf(data);
+ }
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
void verify(WidgetTester tester, List<Offset> idealPositions, List<bool> idealVisibles) {
final List<Offset> actualPositions = tester.renderObjectList<RenderBox>(find.byType(SizedBox, skipOffstage: false)).map<Offset>(
(RenderBox target) => target.localToGlobal(const Offset(0.0, 0.0))
@@ -196,6 +227,47 @@
expect(find.text('BOTTOM'), findsOneWidget);
});
+ testWidgets('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async {
+ final List<String> items = <String>['1', '2', '3', '4', '5', '6'];
+ await testSliverFixedExtentList(tester, items);
+ // Keep alive widgets require 1 frame to notify their parents. Pumps in between
+ // drags to ensure widgets are kept alive.
+ await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -1200.0));
+ await tester.pump();
+ await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -1200.0));
+ await tester.pump();
+ await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -800.0));
+ await tester.pump();
+ expect(find.text('1'), findsNothing);
+ expect(find.text('2'), findsNothing);
+ expect(find.text('3'), findsNothing);
+ expect(find.text('4'), findsOneWidget);
+ expect(find.text('5'), findsOneWidget);
+ // Indexes [0, 1, 2] are kept alive and [3, 4] are in viewport, thus the sliver
+ // will need to keep updating the elements at these indexes whenever a rebuild is
+ // triggered. The current child list in RenderSliverFixedExtentList is
+ // '4' -> '5' -> null.
+ //
+ // With the insertion below, all items will get shifted back 1 position. The sliver
+ // will have to update indexes [0, 1, 2, 3, 4, 5]. Since this is the first time
+ // item '0' gets initialized, mounting the element will cause it to attach to
+ // child list in RenderSliverFixedExtentList. This will create a gap.
+ // '0' -> '4' -> '5' -> null.
+ items.insert(0, '0');
+ await testSliverFixedExtentList(tester, items);
+ // Sliver should collect leading and trailing garbage correctly.
+ //
+ // The child list update should occur in following order.
+ // '0' -> '4' -> '5' -> null Started with Original list.
+ // '4' -> null Removed 1 leading garbage and 1 trailing garbage.
+ // '3' -> '4' -> null Prepended '3' because viewport is still at [3, 4].
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsNothing);
+ expect(find.text('2'), findsNothing);
+ expect(find.text('3'), findsOneWidget);
+ expect(find.text('4'), findsOneWidget);
+ });
+
testWidgets('SliverGrid Correctly layout children after rearranging', (WidgetTester tester) async {
await tester.pumpWidget(const TestSliverGrid(
<Widget>[
@@ -332,3 +404,23 @@
);
}
}
+
+class KeepAlive extends StatefulWidget {
+ const KeepAlive(this.data);
+
+ final String data;
+
+ @override
+ KeepAliveState createState() => KeepAliveState();
+}
+
+class KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin {
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Text(widget.data);
+ }
+}