Skip to content

Commit f9c8717

Browse files
xsahil03xclaude
andcommitted
fix(ui): stop StreamMessageListView from tripping RenderViewport layout-cycle limit
ScrollablePositionedList's reanchor used to reset pixels to 0, which forced the trailing SliverList to walk back through its children one layout cycle at a time on every mid-list anchored layout — exhausting RenderViewport's 10-cycle budget. The reanchor now folds the existing pixels into the new viewport anchor (LazyListState-style) so the slivers don't need to walk back. UnboundedRenderViewport also gains a precisionErrorTolerance guard on sliver corrections and a higher cycle ceiling as belt-and-suspenders. Also pulls the v10 padding fix forward: ScrollablePositionedList.padding now accepts EdgeInsetsGeometry (resolved against Directionality), and scrollTo adjusts for leading padding so the target lands at the content-area edge. See google/flutter.widgets#525, flutter/flutter#30809, flutter/flutter#23527. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7f0804d commit f9c8717

4 files changed

Lines changed: 221 additions & 116 deletions

File tree

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44

55
- Fixed `StreamMessageListView` not auto-scrolling to the bottom on the user's own outgoing message
66
until the server confirmed it.
7-
- Fixed a `FlutterError` ("A RenderViewport exceeded its maximum number of layout cycles") that
8-
could occur when fast-scrolling through the message list.
7+
- Fixed `StreamMessageListView` tripping `A RenderViewport exceeded its maximum number of layout
8+
cycles` under mid-list anchored layout. `ScrollablePositionedList` now preserves the scroll offset
9+
across reanchors instead of resetting it to 0.
10+
11+
🚀 Improved
12+
13+
- `ScrollablePositionedList.padding` now accepts `EdgeInsetsGeometry` (resolved against
14+
`Directionality`), and `scrollTo` lands the target at the content-area edge by adjusting for
15+
leading padding.
916

1017
## 9.24.0
1118

packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart

Lines changed: 101 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class PositionedList extends StatefulWidget {
125125
final bool addSemanticIndexes;
126126

127127
/// The amount of space by which to inset the children.
128-
final EdgeInsets? padding;
128+
final EdgeInsetsGeometry? padding;
129129

130130
/// Whether to wrap each child in a [RepaintBoundary].
131131
///
@@ -452,57 +452,82 @@ class _PositionedListState extends State<PositionedList> {
452452
);
453453
}
454454

455-
EdgeInsets get _leadingSliverPadding =>
456-
(widget.scrollDirection == Axis.vertical
457-
? widget.reverse
458-
? widget.padding?.copyWith(top: 0)
459-
: widget.padding?.copyWith(bottom: 0)
460-
: widget.reverse
461-
? widget.padding?.copyWith(left: 0)
462-
: widget.padding?.copyWith(right: 0)) ??
455+
EdgeInsets get _resolvedPadding =>
456+
widget.padding
457+
?.resolve(Directionality.maybeOf(context) ?? TextDirection.ltr) ??
463458
EdgeInsets.zero;
464459

465-
EdgeInsets get _centerSliverPadding => widget.scrollDirection == Axis.vertical
466-
? widget.reverse
467-
? widget.padding?.copyWith(
468-
top: widget.positionedIndex == widget.itemCount - 1
469-
? widget.padding!.top
470-
: 0,
471-
bottom:
472-
widget.positionedIndex == 0 ? widget.padding!.bottom : 0,
473-
) ??
474-
EdgeInsets.zero
475-
: widget.padding?.copyWith(
476-
top: widget.positionedIndex == 0 ? widget.padding!.top : 0,
477-
bottom: widget.positionedIndex == widget.itemCount - 1
478-
? widget.padding!.bottom
479-
: 0,
480-
) ??
481-
EdgeInsets.zero
482-
: widget.reverse
483-
? widget.padding?.copyWith(
484-
left: widget.positionedIndex == widget.itemCount - 1
485-
? widget.padding!.left
486-
: 0,
487-
right: widget.positionedIndex == 0 ? widget.padding!.right : 0,
488-
) ??
489-
EdgeInsets.zero
490-
: widget.padding?.copyWith(
491-
left: widget.positionedIndex == 0 ? widget.padding!.left : 0,
492-
right: widget.positionedIndex == widget.itemCount - 1
493-
? widget.padding!.right
494-
: 0,
495-
) ??
496-
EdgeInsets.zero;
497-
498-
EdgeInsets get _trailingSliverPadding =>
499-
widget.scrollDirection == Axis.vertical
500-
? widget.reverse
501-
? widget.padding?.copyWith(bottom: 0) ?? EdgeInsets.zero
502-
: widget.padding?.copyWith(top: 0) ?? EdgeInsets.zero
503-
: widget.reverse
504-
? widget.padding?.copyWith(right: 0) ?? EdgeInsets.zero
505-
: widget.padding?.copyWith(left: 0) ?? EdgeInsets.zero;
460+
AxisDirection get _axisDirection {
461+
if (widget.scrollDirection == Axis.vertical) {
462+
return widget.reverse ? AxisDirection.up : AxisDirection.down;
463+
}
464+
final ltr = (Directionality.maybeOf(context) ?? TextDirection.ltr) ==
465+
TextDirection.ltr;
466+
if (widget.reverse) {
467+
return ltr ? AxisDirection.left : AxisDirection.right;
468+
}
469+
return ltr ? AxisDirection.right : AxisDirection.left;
470+
}
471+
472+
EdgeInsets _stripAxisTrailing(EdgeInsets base) {
473+
switch (_axisDirection) {
474+
case AxisDirection.up:
475+
return base.copyWith(top: 0);
476+
case AxisDirection.down:
477+
return base.copyWith(bottom: 0);
478+
case AxisDirection.left:
479+
return base.copyWith(left: 0);
480+
case AxisDirection.right:
481+
return base.copyWith(right: 0);
482+
}
483+
}
484+
485+
EdgeInsets _stripAxisLeading(EdgeInsets base) {
486+
switch (_axisDirection) {
487+
case AxisDirection.up:
488+
return base.copyWith(bottom: 0);
489+
case AxisDirection.down:
490+
return base.copyWith(top: 0);
491+
case AxisDirection.left:
492+
return base.copyWith(right: 0);
493+
case AxisDirection.right:
494+
return base.copyWith(left: 0);
495+
}
496+
}
497+
498+
EdgeInsets get _leadingSliverPadding => _stripAxisTrailing(_resolvedPadding);
499+
500+
EdgeInsets get _trailingSliverPadding => _stripAxisLeading(_resolvedPadding);
501+
502+
// Center keeps cross-axis padding always; carries the leading/trailing
503+
// edge padding only when it owns the first/last item.
504+
EdgeInsets get _centerSliverPadding {
505+
final resolved = _resolvedPadding;
506+
final isFirst = widget.positionedIndex == 0;
507+
final isLast = widget.positionedIndex == widget.itemCount - 1;
508+
switch (_axisDirection) {
509+
case AxisDirection.up:
510+
return resolved.copyWith(
511+
top: isLast ? resolved.top : 0,
512+
bottom: isFirst ? resolved.bottom : 0,
513+
);
514+
case AxisDirection.down:
515+
return resolved.copyWith(
516+
top: isFirst ? resolved.top : 0,
517+
bottom: isLast ? resolved.bottom : 0,
518+
);
519+
case AxisDirection.left:
520+
return resolved.copyWith(
521+
left: isLast ? resolved.left : 0,
522+
right: isFirst ? resolved.right : 0,
523+
);
524+
case AxisDirection.right:
525+
return resolved.copyWith(
526+
left: isFirst ? resolved.left : 0,
527+
right: isLast ? resolved.right : 0,
528+
);
529+
}
530+
}
506531

507532
void _schedulePositionNotificationUpdate() {
508533
if (!updateScheduled) {
@@ -551,32 +576,36 @@ class _PositionedListState extends State<PositionedList> {
551576
final itemOffset = reveal -
552577
viewport.offset.pixels +
553578
anchor * viewport.size.height;
554-
positions.add(ItemPosition(
555-
index: key.index,
556-
itemLeadingEdge: itemOffset.round() /
557-
scrollController.position.viewportDimension,
558-
itemTrailingEdge: (itemOffset + box.size.height).round() /
559-
scrollController.position.viewportDimension,
560-
));
579+
positions.add(
580+
ItemPosition(
581+
index: key.index,
582+
itemLeadingEdge: itemOffset.round() /
583+
scrollController.position.viewportDimension,
584+
itemTrailingEdge: (itemOffset + box.size.height).round() /
585+
scrollController.position.viewportDimension,
586+
),
587+
);
561588
} else {
562589
final itemOffset =
563590
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
564591
if (!itemOffset.isFinite) continue;
565-
positions.add(ItemPosition(
566-
index: key.index,
567-
itemLeadingEdge: (widget.reverse
568-
? scrollController.position.viewportDimension -
569-
(itemOffset + box.size.width)
570-
: itemOffset)
571-
.round() /
572-
scrollController.position.viewportDimension,
573-
itemTrailingEdge: (widget.reverse
574-
? scrollController.position.viewportDimension -
575-
itemOffset
576-
: (itemOffset + box.size.width))
577-
.round() /
578-
scrollController.position.viewportDimension,
579-
));
592+
positions.add(
593+
ItemPosition(
594+
index: key.index,
595+
itemLeadingEdge: (widget.reverse
596+
? scrollController.position.viewportDimension -
597+
(itemOffset + box.size.width)
598+
: itemOffset)
599+
.round() /
600+
scrollController.position.viewportDimension,
601+
itemTrailingEdge: (widget.reverse
602+
? scrollController.position.viewportDimension -
603+
itemOffset
604+
: (itemOffset + box.size.width))
605+
.round() /
606+
scrollController.position.viewportDimension,
607+
),
608+
);
580609
}
581610
} on TypeError catch (_) {
582611
// Specifically the null-check failure inside

0 commit comments

Comments
 (0)