Skip to content

fix(ui): stop StreamMessageListView from tripping RenderViewport layout-cycle limit#2703

Merged
xsahil03x merged 2 commits into
masterfrom
fix/spl-render-viewport-layout-cycles
Jun 1, 2026
Merged

fix(ui): stop StreamMessageListView from tripping RenderViewport layout-cycle limit#2703
xsahil03x merged 2 commits into
masterfrom
fix/spl-render-viewport-layout-cycles

Conversation

@xsahil03x
Copy link
Copy Markdown
Member

@xsahil03x xsahil03x commented Jun 1, 2026

Summary

  • ScrollablePositionedList reanchor (_updateFirstVisibleItemIfNeeded) no longer resets pixels to 0 after a list mutation; it folds the existing scroll offset into the new viewport anchor so the underlying slivers don't have to walk back through their children one layout cycle at a time.
  • UnboundedRenderViewport ignores sub-precisionErrorTolerance sliver corrections (which can't actually move pixels) and raises its per-frame cycle ceiling from 10 to 100 as a safety net.
  • Forward-ports the v10 padding fix: ScrollablePositionedList.padding now accepts EdgeInsetsGeometry (resolved against Directionality), and scrollTo lands the target at the content-area edge by adjusting for leading padding. Also fixes a few edge cases in that forward-port flagged in review (empty-list padding symmetry, RTL ItemPosition math, scrollTo padding-adjust for non-reversed lists).
  • Adds .fvmrc pinning the workspace to the melos-declared Flutter floor so contributors running fvm pick up the right SDK automatically.

Background

StreamMessageListView was hitting A RenderViewport exceeded its maximum number of layout cycles under mid-list anchored layout — repro'd in production. Each time _updateFirstVisibleItemIfNeeded reset pixels to 0 after an item-set change, the underlying slivers had to walk back through their kept-alive children one correction per frame; with a high enough starting firstChild index, the viewport's 10-cycle budget was exhausted.

The math for the fix: solve newAnchor * viewport - pixels == currentAlignment * viewport so the anchored item lands at the same visual position without touching pixels. No walk-back, no cycle budget pressure.

Test plan

  • flutter test packages/stream_chat_flutter/test/scrollable_positioned_list/ — 194/194 pass (2 pre-existing skips)
  • melos run analyze — no new issues from these changes
  • Manual: mid-list anchored layout with itemKeyBuilder set + a stream of mutations doesn't hit the assert
  • Manual: scrollTo with padding lands the target at the content-area edge (not the viewport edge)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed auto-scrolling delay for outgoing messages until server confirmation
    • Resolved layout reanchor cycles causing viewport rendering issues
    • Improved floating-point precision handling in layout calculations
  • Improvements

    • Enhanced padding configuration with better directional support
    • Corrected scroll positioning to properly align content at viewport edges
  • Chores

    • Updated Flutter SDK to version 3.27.4

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Expands padding API to accept EdgeInsetsGeometry (resolved via Directionality), implements axis-aware padding and content-edge scroll adjustments, preserves visual anchor alignment without resetting scroll pixels, refactors item-position calculations, and relaxes viewport layout-cycle convergence checks.

Changes

ScrollablePositionedList API and Behavior Update

Layer / File(s) Summary
Public padding API type expansion
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart, packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
padding types updated from EdgeInsets? to EdgeInsetsGeometry? in both PositionedList and ScrollablePositionedList.
Padding resolution and sliver application
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart
Resolve EdgeInsetsGeometry with ambient Directionality, compute axis-aware leading/trailing edges, and conditionally apply leading/trailing padding to centered slivers based on positioned item index.
ItemPosition calculation refactor
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart
Refactor _schedulePositionNotificationUpdate to compute a local itemOffset and recompute leading/trailing edge math for vertical and horizontal modes using AxisDirection and viewport anchor.
Anchor preservation without pixel reset
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
Refactor _updateFirstVisibleItemIfNeeded to solve for a new primary.alignment that preserves the current visual pixel baseline without mutating pixels; update saved alignment baseline handling and comments.
Content-edge alignment via padding adjustment
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
Add direction-aware helpers to resolve leading padding and subtract a leading-padding adjustment in _startScroll when revealing the leading-end item (index==0, alignment==0) so it lands at the content-area edge.
Long-distance scroll path and position filtering
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
Reformat long-distance transition/jump expressions and the itemPositions filtering predicate; logic preserved.
Viewport layout-cycle and correction convergence
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart
Import package:flutter/foundation.dart; increase _maxLayoutCycles from 10 to 100 and treat offset corrections within precisionErrorTolerance as converged to avoid spurious iterations.
Changelog and repository configuration
packages/stream_chat_flutter/CHANGELOG.md, .fvmrc
CHANGELOG documents the targeted StreamMessageListView fixes and padding/scroll-to-edge improvements; .fvmrc pins Flutter SDK to 3.27.4.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • GetStream/stream-chat-flutter#2693: Also modifies UnboundedRenderViewport.performLayout layout-cycle/iteration handling to prevent layout-cycle budget exhaustion during anchor/scroll corrections.
  • GetStream/stream-chat-flutter#2651: Alters related scrolling/positioning internals in scrollable_positioned_list, overlapping the message list positioning surface.

Suggested reviewers

  • renefloor
  • martinmitrevski

"🐰 I nudged the padding wide,
anchors hold while pixels bide,
scrolls arrive on edge so neat,
rtl and ltr all meet,
layout calmed — hop, hush, repeat."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main fix: preventing StreamMessageListView from exceeding the RenderViewport layout-cycle limit, which is the primary issue addressed across multiple file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/spl-render-viewport-layout-cycles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 92.15686% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.67%. Comparing base (03c8827) to head (448ad4b).

Files with missing lines Patch % Lines
...ositioned_list/src/scrollable_positioned_list.dart 81.25% 6 Missing ⚠️
...crollable_positioned_list/src/positioned_list.dart 97.10% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2703      +/-   ##
==========================================
+ Coverage   65.66%   65.67%   +0.01%     
==========================================
  Files         423      423              
  Lines       26666    26694      +28     
==========================================
+ Hits        17509    17532      +23     
- Misses       9157     9162       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart (1)

222-231: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isScrollingListenable is not actually safe to subscribe to before attach.

Returning a noop object here drops any listener added before _attach, so the documented “register at any time” contract is false. This needs a stable controller-owned notifier/proxy instead of swapping the listenable instance on attachment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart`
around lines 222 - 231, The getter isScrollingListenable currently returns a
_NoopListenable until _attach, which drops listeners added early; fix by
creating a persistent controller-owned proxy/listenable (e.g. a private
ValueNotifier<bool> or proxy Listenable field) that the getter always returns
instead of swapping instances, then when _attach is called subscribe the
attached state's _isScrollingListenable (or _isScrolling notifier) and forward
its value/notifications into that proxy; ensure you wire up detach/dispose to
remove subscriptions and keep symbol references: isScrollingListenable getter,
_scrollableListState, _isScrollingListenable, _NoopListenable (remove use), and
the new controller-owned proxy field so listeners registered before attach are
preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart`:
- Around line 51-54: The constructor assertion in PositionedList currently
allows negative positionedIndex values; update the assertion in PositionedList
(the constructor in positioned_list.dart) to reject negatives by requiring
positionedIndex >= 0 and positionedIndex < itemCount (i.e., tighten the lower
bound so invalid negative indices fail at the API boundary rather than during
layout).

In
`@packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart`:
- Around line 680-684: The helper _resolveLeadingEndPaddingAdjust currently
computes isAtLeadingEnd as "widget.reverse ? index == 0 : index ==
widget.itemCount - 1", which is inverted; for non-reversed lists the leading end
is index == 0 and for reversed lists it is index == widget.itemCount - 1. Update
the condition in _resolveLeadingEndPaddingAdjust (the isAtLeadingEnd
calculation) to the correct form (e.g. widget.reverse ? index ==
widget.itemCount - 1 : index == 0) so the leading-padding correction applies to
the actual leading item.

---

Outside diff comments:
In
`@packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart`:
- Around line 222-231: The getter isScrollingListenable currently returns a
_NoopListenable until _attach, which drops listeners added early; fix by
creating a persistent controller-owned proxy/listenable (e.g. a private
ValueNotifier<bool> or proxy Listenable field) that the getter always returns
instead of swapping instances, then when _attach is called subscribe the
attached state's _isScrollingListenable (or _isScrolling notifier) and forward
its value/notifications into that proxy; ensure you wire up detach/dispose to
remove subscriptions and keep symbol references: isScrollingListenable getter,
_scrollableListState, _isScrollingListenable, _NoopListenable (remove use), and
the new controller-owned proxy field so listeners registered before attach are
preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58985ee7-2ff9-4d5e-a5b3-3a80d518934c

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0804d and 730e710.

📒 Files selected for processing (4)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart

@xsahil03x xsahil03x force-pushed the fix/spl-render-viewport-layout-cycles branch 3 times, most recently from 7d34ab2 to 8a2f56d Compare June 1, 2026 15:53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart`:
- Around line 504-529: The center sliver padding logic drops one main-axis edge
when widget.itemCount == 0 because isLast becomes false; update
_centerSliverPadding to preserve both edges for empty lists by checking
widget.itemCount == 0 (or equivalently treating isFirst and isLast as true) and
returning the full _resolvedPadding in that case before the AxisDirection
switch; modify the function around _centerSliverPadding/_resolvedPadding and
usages of widget.positionedIndex and widget.itemCount to handle the empty-list
early return.
- Around line 592-606: The leading/trailing edge math for horizontal items
currently branches on widget.reverse causing RTL to be flipped; update the logic
that builds ItemPosition (itemLeadingEdge/itemTrailingEdge) to branch on
_axisDirection instead, using AxisDirection.right to compute leading =
itemOffset and trailing = itemOffset + box.size.width, and AxisDirection.left to
compute leading = viewportDimension - (itemOffset + box.size.width) and trailing
= viewportDimension - itemOffset (still divide by
scrollController.position.viewportDimension and round as before); change the
conditional checks around itemOffset/box.size.width in the ItemPosition
construction to reference _axisDirection (AxisDirection.left/right) rather than
widget.reverse so RTL is correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf3e7d48-67b0-495c-9fcc-1eefeb430630

📥 Commits

Reviewing files that changed from the base of the PR and between 730e710 and ad5b72b.

📒 Files selected for processing (6)
  • .fvmrc
  • .gitignore
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart
✅ Files skipped from review due to trivial changes (3)
  • .fvmrc
  • .gitignore
  • packages/stream_chat_flutter/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart

@xsahil03x xsahil03x force-pushed the fix/spl-render-viewport-layout-cycles branch 2 times, most recently from 8d883ce to 219f2c3 Compare June 1, 2026 16:02
Copy link
Copy Markdown
Contributor

@Brazol Brazol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@xsahil03x xsahil03x force-pushed the fix/spl-render-viewport-layout-cycles branch 3 times, most recently from 8ba11f0 to 76b2f66 Compare June 1, 2026 16:28
@xsahil03x xsahil03x enabled auto-merge (squash) June 1, 2026 16:29
@xsahil03x xsahil03x disabled auto-merge June 1, 2026 16:32
xsahil03x and others added 2 commits June 1, 2026 18:33
…ut-cycle limit

ScrollablePositionedList's reanchor used to reset pixels to 0, which
forced the underlying slivers to walk back through their children one
layout cycle at a time on mid-list anchored layouts — exhausting
RenderViewport's 10-cycle budget. The reanchor now folds the existing
pixels into the new viewport anchor so no walk-back is needed.
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, and scrollTo adjusts for leading padding
so the target lands at the content-area edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds .fvmrc with the same Flutter floor the workspace declares in
melos.yaml (>=3.27.4) so contributors running fvm pick up the
right SDK automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x force-pushed the fix/spl-render-viewport-layout-cycles branch from 76b2f66 to 448ad4b Compare June 1, 2026 16:33
@xsahil03x xsahil03x merged commit 58331ec into master Jun 1, 2026
20 checks passed
@xsahil03x xsahil03x deleted the fix/spl-render-viewport-layout-cycles branch June 1, 2026 16:43
xsahil03x added a commit that referenced this pull request Jun 1, 2026
Resolves SPL conflicts in scrollable_positioned_list and positioned_list:
- Take master's preserve-pixels `_updateFirstVisibleItemIfNeeded` (PR #2703)
- Take master's empty-list `_centerSliverPadding` guard
- Take master's RTL `_axisDirection`-based `_updatePositions` branching
- Take master's `_resolveLeadingEndPaddingAdjust` `index != 0` check
- Drop orphaned `message_card_test.dart` (widget deleted in v10 design refresh);
  master's MessageCard fix from #2702 ported to StreamMessageContent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants