Skip to content
This repository was archived by the owner on Oct 13, 2025. It is now read-only.

Commit eaa59c7

Browse files
Skip an Element if its RenderObject has never been laid out (#409)
* internal b/183417182 Skip an Element if its RenderObject has never been laid out * bump scrollable positioned list to 0.3.3
1 parent 3896dc7 commit eaa59c7

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

packages/scrollable_positioned_list/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 0.3.3
2+
* Fix potential crash when reading from RenderBox.size.
3+
14
# 0.3.2
25
* Re-apply Flutter framework bindings' null safety calls but set SDK
36
constraints correctly to 2.12.0 instead.

packages/scrollable_positioned_list/lib/src/positioned_list.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,13 +309,14 @@ class _PositionedListState extends State<PositionedList> {
309309
if (!updateScheduled) {
310310
updateScheduled = true;
311311
SchedulerBinding.instance.addPostFrameCallback((_) {
312-
if (registeredElements.value == null) {
312+
final elements = registeredElements.value;
313+
if (elements == null) {
313314
updateScheduled = false;
314315
return;
315316
}
316317
final positions = <ItemPosition>[];
317318
RenderViewportBase? viewport;
318-
for (var element in registeredElements.value!) {
319+
for (var element in elements) {
319320
final RenderBox box = element.renderObject as RenderBox;
320321
viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?;
321322
var anchor = 0.0;
@@ -328,6 +329,8 @@ class _PositionedListState extends State<PositionedList> {
328329
}
329330

330331
final ValueKey<int> key = element.widget.key as ValueKey<int>;
332+
// Skip this element if `box` has never been laid out.
333+
if (!box.hasSize) continue;
331334
if (widget.scrollDirection == Axis.vertical) {
332335
final reveal = viewport!.getOffsetToReveal(box, 0).offset;
333336
if (!reveal.isFinite) continue;

packages/scrollable_positioned_list/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: scrollable_positioned_list
2-
version: 0.3.2
2+
version: 0.3.3
33
description: >
44
A list with helper methods to programmatically scroll to an item.
55
homepage: https://github.com/google/flutter.widgets/tree/master/packages/scrollable_positioned_list

packages/scrollable_positioned_list/test/positioned_list_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,55 @@ void main() {
360360
.itemTrailingEdge,
361361
1);
362362
});
363+
364+
testWidgets('Does not crash when updated offscreen',
365+
(WidgetTester tester) async {
366+
late var setState;
367+
bool updated = false;
368+
369+
// There's 0 relayout boundaries in this subtree.
370+
final widget = StatefulBuilder(builder: (context, stateSetter) {
371+
setState = stateSetter;
372+
return Positioned(
373+
left: 0,
374+
right: 0,
375+
child: PositionedList(
376+
shrinkWrap: true,
377+
itemCount: 1,
378+
// When `updated` becomes true this line inserts a
379+
// RenderIndexedSemantics to the render tree.
380+
addSemanticIndexes: updated,
381+
itemBuilder: (context, index) => SizedBox(height: itemHeight),
382+
));
383+
});
384+
385+
await tester.pumpWidget(Directionality(
386+
textDirection: TextDirection.ltr,
387+
child: Overlay(
388+
initialEntries: [
389+
OverlayEntry(builder: (context) => widget, maintainState: true),
390+
],
391+
),
392+
));
393+
394+
// Insert a new opaque OverlayEntry that would prevent the first OverlayEntry
395+
// from doing re-layout. Since there's no relayout boundaries in the first
396+
// OverlayEntry, no dirty RenderObjects in its render subtree can update
397+
// layout.
398+
final newOverlay =
399+
OverlayEntry(builder: (context) => SizedBox.expand(), opaque: true);
400+
tester.state<OverlayState>(find.byType(Overlay)).insert(newOverlay);
401+
await tester.pump();
402+
403+
// Update the list item's render tree. A new RenderObjectElement is
404+
// inflated, registeredElement.renderObject will point to this new
405+
// RenderObjectElement's RenderObject (RenderIndexedSemantics), which has
406+
// never been laid out.
407+
setState(() {
408+
updated = true;
409+
});
410+
411+
await tester.pump();
412+
expect(tester.takeException(), isNull);
413+
});
363414
}

0 commit comments

Comments
 (0)