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 2b3eefba51..628ca0af9a 100644 --- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart @@ -148,6 +148,13 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) { final RenderBox child = insertAndLayoutLeadingChild(childConstraints); + if (child == null) { + // Items before the previously first child are no longer present. + // Reset the scroll offset to offset all items prior and up to the + // missing item. Let parent re-layout everything. + geometry = new SliverGeometry(scrollOffsetCorrection: index * itemExtent); + return; + } final SliverMultiBoxAdaptorParentData childParentData = child.parentData; childParentData.layoutOffset = indexToLayoutOffset(itemExtent, index); assert(childParentData.index == index); diff --git a/packages/flutter/test/widgets/slivers_evil_test.dart b/packages/flutter/test/widgets/slivers_evil_test.dart index 8ccc9f6f57..214031e833 100644 --- a/packages/flutter/test/widgets/slivers_evil_test.dart +++ b/packages/flutter/test/widgets/slivers_evil_test.dart @@ -194,4 +194,67 @@ void main() { await tester.pumpAndSettle(const Duration(milliseconds: 122)); }); + + testWidgets('Removing offscreen items above and rescrolling does not crash', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp( + home: new CustomScrollView( + slivers: [ + new SliverFixedExtentList( + itemExtent: 100.0, + delegate: new SliverChildBuilderDelegate( + (BuildContext context, int index) { + return new Container( + color: Colors.blue, + child: new Text(index.toString()), + ); + }, + childCount: 30, + ), + ), + ], + ), + )); + + await tester.drag(find.text('5'), const Offset(0.0, -500.0)); + await tester.pump(); + + // Screen is 600px high. Moved bottom item 500px up. It's now at the top. + expect(tester.getTopLeft(find.widgetWithText(DecoratedBox, '5')).dy, 0.0); + expect(tester.getBottomLeft(find.widgetWithText(DecoratedBox, '10')).dy, 600.0); + + // Stop returning the first 3 items. + await tester.pumpWidget(new MaterialApp( + home: new CustomScrollView( + slivers: [ + new SliverFixedExtentList( + itemExtent: 100.0, + delegate: new SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index > 3) { + return new Container( + color: Colors.blue, + child: new Text(index.toString()), + ); + } + return null; + }, + childCount: 30, + ), + ), + ], + ), + )); + + await tester.drag(find.text('5'), const Offset(0.0, 400.0)); + await tester.pump(); + + // Move up by 4 items, meaning item 1 would have been at the top but + // 0 through 3 no longer exist, so item 4, 3 items down, is the first one. + // Item 4 is also shifted to the top. + expect(tester.getTopLeft(find.widgetWithText(DecoratedBox, '4')).dy, 0.0); + + // Because the screen is still 600px, item 9 is now visible at the bottom instead + // of what's supposed to be item 6 had we not re-shifted. + expect(tester.getBottomLeft(find.widgetWithText(DecoratedBox, '9')).dy, 600.0); + }); }