diff --git a/packages/flutter/lib/src/rendering/sliver_grid.dart b/packages/flutter/lib/src/rendering/sliver_grid.dart index b2141dcd55..524dd70e4d 100644 --- a/packages/flutter/lib/src/rendering/sliver_grid.dart +++ b/packages/flutter/lib/src/rendering/sliver_grid.dart @@ -596,14 +596,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { final int firstIndex = layout.getMinChildIndexForScrollOffset(scrollOffset); final int? targetLastIndex = targetEndScrollOffset.isFinite ? layout.getMaxChildIndexForScrollOffset(targetEndScrollOffset) : null; - if (firstChild != null) { - final int oldFirstIndex = indexOf(firstChild!); - final int oldLastIndex = indexOf(lastChild!); - final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount); // ignore_clamp_double_lint - final int trailingGarbage = targetLastIndex == null - ? 0 - : (oldLastIndex - targetLastIndex).clamp(0, childCount); // ignore_clamp_double_lint + final int leadingGarbage = _calculateLeadingGarbage(firstIndex); + final int trailingGarbage = targetLastIndex != null ? _calculateTrailingGarbage(targetLastIndex) : 0; collectGarbage(leadingGarbage, trailingGarbage); } else { collectGarbage(0, 0); @@ -713,4 +708,24 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { } childManager.didFinishLayout(); } + + 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; + } } diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index 71769d00d6..f0f3d72fbd 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -232,6 +232,69 @@ void main() { expect(find.text('BOTTOM'), findsOneWidget); }); + testWidgetsWithLeakTracking('Sliver grid can replace intermediate items', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/138749. + // The bug happens when items in between first and last item changed while + // the sliver layout only display a item in the middle of the list. + final List items = [0, 1, 2, 3, 4, 5]; + final List replacedItems = [0, 2, 9, 10, 11, 12, 5]; + Future pumpSliverGrid(bool replace) async { + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200, + height: 200, + child: Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverGrid( + gridDelegate: TestGridDelegate(replace), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final int item = replace + ? replacedItems[index] + : items[index]; + return Container( + key: ValueKey(item), + alignment: Alignment.center, + child: Text('item $item'), + ); + }, + childCount: replace ? 7 : 6, + findChildIndexCallback: (Key key) { + final int item = (key as ValueKey).value; + final int index = replace + ? replacedItems.indexOf(item) + : items.indexOf(item); + return index >= 0 ? index : null; + }, + ), + ), + ], + ), + ), + ), + ), + ); + } + + await pumpSliverGrid(false); + expect(find.text('item 0'), findsOneWidget); + expect(find.text('item 1'), findsOneWidget); + expect(find.text('item 2'), findsOneWidget); + expect(find.text('item 3'), findsOneWidget); + expect(find.text('item 4'), findsOneWidget); + + await pumpSliverGrid(true); + // The TestGridDelegate only show child at index 1 when not expand. + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsOneWidget); + expect(find.text('item 3'), findsNothing); + expect(find.text('item 4'), findsNothing); + }); + testWidgetsWithLeakTracking('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async { final List items = ['1', '2', '3', '4', '5', '6']; await testSliverFixedExtentList(tester, items); @@ -1504,3 +1567,56 @@ class _NullBuildContext implements BuildContext { @override dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError(); } + +class TestGridDelegate implements SliverGridDelegate { + TestGridDelegate(this.replace); + + final bool replace; + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + return TestGridLayout(replace); + } + + @override + bool shouldRelayout(covariant TestGridDelegate oldDelegate) { + return true; + } +} + +class TestGridLayout implements SliverGridLayout { + TestGridLayout(this.replace); + + final bool replace; + + @override + double computeMaxScrollOffset(int childCount) { + return 200; + } + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + return SliverGridGeometry( + crossAxisOffset: 20.0 + 20 * index, + crossAxisExtent: 20, + mainAxisExtent: 20, + scrollOffset: 0, + ); + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + if (replace) { + return 1; + } + return 5; + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + if (replace) { + return 1; + } + return 0; + } +}