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].

<!-- Links -->
[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
This commit is contained in:
Matej Knopp 2024-03-13 22:28:43 +01:00 committed by GitHub
parent fd73a9a0c6
commit 19087442ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 3 deletions

View file

@ -1385,7 +1385,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
return constraints.biggest;
}
static const int _maxLayoutCycles = 10;
static const int _maxLayoutCyclesPerChild = 10;
// Out-of-band data computed during layout.
late double _minScrollExtent;
@ -1419,6 +1419,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
};
final double centerOffsetAdjustment = center!.centerOffsetAdjustment;
final int maxLayoutCycles = _maxLayoutCyclesPerChild * childCount;
double correction;
int count = 0;
@ -1435,9 +1436,9 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
}
}
count += 1;
} while (count < _maxLayoutCycles);
} while (count < maxLayoutCycles);
assert(() {
if (count >= _maxLayoutCycles) {
if (count >= maxLayoutCycles) {
assert(count != 1);
throw FlutterError(
'A RenderViewport exceeded its maximum number of layout cycles.\n'

View file

@ -2370,4 +2370,85 @@ void main() {
);
errors.clear();
});
testWidgets('RenderViewport maxLayoutCycles depends on the number of children',
(WidgetTester tester) async {
Future<void> expectFlutterError({
required Widget widget,
required WidgetTester tester,
}) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
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<Widget>.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;
}
}