Skip to content

fix: bug with KeyboardChatScrollView + inverted + maintainVisibleScrollPosition + FlashList#1437

Merged
kirillzyusko merged 5 commits intomainfrom
fix/flash-list+inverted+maintainVisibleScrollPosition+iOS
Apr 20, 2026
Merged

fix: bug with KeyboardChatScrollView + inverted + maintainVisibleScrollPosition + FlashList#1437
kirillzyusko merged 5 commits intomainfrom
fix/flash-list+inverted+maintainVisibleScrollPosition+iOS

Conversation

@kirillzyusko
Copy link
Copy Markdown
Owner

@kirillzyusko kirillzyusko commented Apr 16, 2026

📜 Description

Fixed a problem with non-working maintainVisibleScrollPosition when using inverted prop of KeyboardChatScrollView.

💡 Motivation and Context

The issue stems from the fact that we use additional View in KeyboardChatScrollView:

<ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
  {inverted ? (
    // The only thing it can break is `StickyHeader`, but it's already broken in FlatList and other lists
    // don't support this functionality, so we can add additional view here
    // The correct fix would be to add a new prop in ScrollView that allows
    // to customize children extraction logic and skip custom view
    <View collapsable={false} nativeID="container">
      {children}
    </View>
  ) : (
    children
  )}
</ScrollViewComponent>

It totally breaks maintainVisibleScrollPosition behavior. The algorithm is similar on both iOS/Android, but we'll use Android code snippets for demonstration. First of all it iterates over direct children of contentView:

for (i in config.minIndexForVisible until contentView.childCount) {
    val child = contentView.getChildAt(i)
    val position = child.y + child.height
    if (position > currentScroll || i == contentView.childCount - 1) {
        firstVisibleViewRef = WeakReference(child)
        prevFirstVisibleFrame = frame
        break
    }
}

So when we draw additional View - the algorithm becomes completely broken. Further the code does something like this:

val deltaY = newFrame.top - prevFirstVisibleFrame.top
if (deltaY != 0) {
    scrollView.scrollToPreservingMomentum(scrollView.scrollX, scrollY + deltaY)
}

But this code will never work properly, because on first step we will not be able to compute proper prevFirstVisibleFrame (since we have only single View and this View is always visible).

First idea that I had in my mind was adding TransientView that would substitute childCount/getChildAt and forward them down to inner children. However it significantly increases the complexity of the app. And I decided to dig deeper into "why exactly I added this code?".

The reason was very simple. My virtualized list example was flickering during keyboard animation (pay attention to "So far it looks cool!" message in the bottom of the screen):

telegram-cloud-document-2-5409326574178701854.mp4

If we check debugger we can see that this item has been recycled:

Screenshot 2026-04-17 at 10 57 35

Important

This issue seems to be reproducible only with FlashList (not FlatList). If I remember correctly FlatList tries to mount all items if we don't provide a function for layout prediction (just do it in batch updates). FlashList actually recycles items (and because of that it has better memory footprint). So the issue is reproducible only in FlashList since it recycles items.

Warning

I also managed to reproduce it on new arch only. Old arch works well even without drawDistance specification 🤷‍♂️

I think this problem is caused by the fact that we change translateY of the content and perform scroll. In this case visible items are not the same that FlashList thinks are visible (because FlashList doesn't know anything about translateY that was modified natively).

So the fix is to remove additional view and simply increase windowSize in example app (I also mentioned this side effect in documentation).

Additionally I think that this fake View may completely break recycling of FlashList because we render a single big View which technically is always visible 🤷‍♂️


So in general: rendering additional View may break many things (maintainVisibleScrollPosition, recycling) so it's not a correct solution. Current solution - remove that fake view AND increase rendering buffer for virtualized lists.

Closes #1423

📢 Changelog

Docs

  • mention that inverted prop may require enhanced drawDistance;

JS

  • removed additional View when rendering inverted list;

Android

  • use contentView directly (no first child of contentView anymore) for modifying translateY posiiton;

🤔 How Has This Been Tested?

Tested manually on Pixel 7 Pro (API 36).

📸 Screenshots (if appropriate):

Flickering

Before After
telegram-cloud-document-2-5409326574178701854.mp4
telegram-cloud-document-2-5409326574178701877.mp4

Auto-scrolling

Before After
telegram-cloud-document-2-5409326574178701952.mp4
telegram-cloud-document-2-5409326574178701946.mp4

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed

@kirillzyusko kirillzyusko self-assigned this Apr 16, 2026
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🍎 iOS iOS specific KeyboardChatScrollView 💬 Anything about chat functionality labels Apr 16, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

📊 Package size report

Current size Target Size Difference
311728 bytes 312781 bytes -1053 bytes 📉

@kirillzyusko kirillzyusko changed the title fix: bug on iOS with KeyboardChatScrollView + inverted + maintainVisibleScrollPosition + FlashList fix: bug with KeyboardChatScrollView + inverted + maintainVisibleScrollPosition + FlashList Apr 16, 2026
@kirillzyusko kirillzyusko force-pushed the fix/flash-list+inverted+maintainVisibleScrollPosition+iOS branch from 301786c to 451d730 Compare April 17, 2026 08:48
@kirillzyusko kirillzyusko added documentation Improvements or additions to documentation 🤖 android Android specific example Anything related to example project deps update Upgrade dependencies of the project and removed 🍎 iOS iOS specific labels Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-04-20 11:07 UTC

@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) 👍 Changes approved 1 changed Apr 17, 2026, 9:31 AM

@kirillzyusko kirillzyusko marked this pull request as ready for review April 17, 2026 10:03
@github-actions
Copy link
Copy Markdown
Contributor

LGTM

@kirillzyusko kirillzyusko merged commit c436fbb into main Apr 20, 2026
32 of 33 checks passed
@kirillzyusko kirillzyusko deleted the fix/flash-list+inverted+maintainVisibleScrollPosition+iOS branch April 20, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Android specific 🐛 bug Something isn't working deps update Upgrade dependencies of the project documentation Improvements or additions to documentation example Anything related to example project KeyboardChatScrollView 💬 Anything about chat functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyboardChatScrollView break inverted FlashLIst maintainVisibleScrollPosition

1 participant