From 19087442ceb788eb64f3203c6e7542dcdb0d9225 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 13 Mar 2024 22:28:43 +0100 Subject: [PATCH] RenderViewport max layout cycles should depend on number of slivers (#144104) Fixes https://github.com/flutter/flutter/issues/144102 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [Data Driven Fixes]: https://github.com/flutter/flutter/wiki/Data-driven-Fixes --- .../flutter/lib/src/rendering/viewport.dart | 7 +- .../flutter/test/rendering/viewport_test.dart | 81 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index e1fa76e4796..45d72611f41 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -1385,7 +1385,7 @@ class RenderViewport extends RenderViewportBase= _maxLayoutCycles) { + if (count >= maxLayoutCycles) { assert(count != 1); throw FlutterError( 'A RenderViewport exceeded its maximum number of layout cycles.\n' diff --git a/packages/flutter/test/rendering/viewport_test.dart b/packages/flutter/test/rendering/viewport_test.dart index 7013d6dcd18..8f80cfc3013 100644 --- a/packages/flutter/test/rendering/viewport_test.dart +++ b/packages/flutter/test/rendering/viewport_test.dart @@ -2370,4 +2370,85 @@ void main() { ); errors.clear(); }); + + testWidgets('RenderViewport maxLayoutCycles depends on the number of children', + (WidgetTester tester) async { + Future expectFlutterError({ + required Widget widget, + required WidgetTester tester, + }) async { + final List errors = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); + try { + await tester.pumpWidget(widget); + } finally { + FlutterError.onError = oldHandler; + } + expect(errors, isNotEmpty); + expect(errors.first.exception, isFlutterError); + } + + Widget buildWidget({required int sliverCount, required int correctionsCount}) { + return Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: List.generate( + sliverCount, + (_) => _ScrollOffsetCorrectionSliver(correctionsCount: correctionsCount)), + ), + ); + } + + // 5 correction per child will pass. + await tester.pumpWidget(buildWidget(sliverCount: 30, correctionsCount: 5)); + + // 15 correction per child will throw exception. + await expectFlutterError( + widget: buildWidget(sliverCount: 1, correctionsCount: 15), + tester: tester, + ); + }); +} + +// Simple sliver that applies N scroll offset corrections. +class _RenderScrollOffsetCorrectionSliver extends RenderSliver { + int _correctionCount = 0; + @override + void performLayout() { + if (_correctionCount > 0) { + --_correctionCount; + geometry = const SliverGeometry(scrollOffsetCorrection: 1.0); + return; + } + const double extent = 5; + final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent); + final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: extent); + + geometry = SliverGeometry( + scrollExtent: extent, + paintExtent: paintedChildSize, + maxPaintExtent: extent, + cacheExtent: cacheExtent + ); + } +} + +class _ScrollOffsetCorrectionSliver extends SingleChildRenderObjectWidget { + const _ScrollOffsetCorrectionSliver({required this.correctionsCount}); + final int correctionsCount; + + @override + _RenderScrollOffsetCorrectionSliver createRenderObject(BuildContext context) { + final _RenderScrollOffsetCorrectionSliver sliver = _RenderScrollOffsetCorrectionSliver(); + sliver._correctionCount = correctionsCount; + return sliver; + } + + @override + void updateRenderObject(BuildContext context, covariant _RenderScrollOffsetCorrectionSliver renderObject) { + super.updateRenderObject(context, renderObject); + renderObject.markNeedsLayout(); + renderObject._correctionCount = correctionsCount; + } }