Share explicit scroll snap selection with Kotlin Multiplatform - #58475
Open
kunal26das wants to merge 10 commits into
Open
Share explicit scroll snap selection with Kotlin Multiplatform#58475kunal26das wants to merge 10 commits into
kunal26das wants to merge 10 commits into
Conversation
This was referenced Sep 10, 2026
kunal26das
force-pushed
the
codex/kmp-scroll-snap-stack
branch
from
September 10, 2026 19:34
265de92 to
772c54b
Compare
kunal26das
force-pushed
the
codex/kmp-scroll-snap-stack
branch
from
September 10, 2026 19:51
772c54b to
9cc8379
Compare
kunal26das
force-pushed
the
codex/kmp-scroll-snap-stack
branch
from
September 10, 2026 21:16
9cc8379 to
514b5b2
Compare
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Native stacked review: Review this layer in the registered six-PR fork stack. This PR remains the upstream submission to
react/react-native.Share explicit
snapToOffsetstarget selection between Android's vertical, horizontal and nested scroll views and Apple's enhanced scroll view. This is the third use case after the KMP foundation #58472, gradients #58456 and multipart framing #58474. Each use case is independently reviewable; there is no Compose dependency.Review this layer: scroll implementation and validation (13 paths). The full upstream diff includes earlier fork-branch prerequisites because GitHub does not support cross-fork stacked bases. The independent native fixes in #58471 and #58473 are also prerequisites in the branch history.
The common selector is 131 lines and returns a small object containing target, selected offset and direction; Apple reads its target offset. Each view retains native prediction, velocity adjustment, overscroll, density, RTL and axis handling, interval snapping and scrolling physics. Apple caches a primitive offset array when the property changes and calls Kotlin once per fling. Android retains the original caller-owned integer list, including subsequent mutations, and preserves its integer overflow behavior. Empty-list behavior is preserved separately for horizontal versus vertical/nested views. Catalyst retains its native path. Apple source builds remain opt-in with
RCT_USE_KMP=1.Why consider this boundary?
The selector is duplicated across three Kotlin views and an Objective-C++ view, and its small target/direction result keeps the interop boundary small. Common tests centralize decision behavior while platform adapters keep their differences explicit. This is a maintenance proposal, not a claim that Kotlin is faster than a well-tuned native implementation. React Native's existing C++ infrastructure and a native caching change are both credible alternatives.
The benchmark therefore includes the original Apple implementation, a test-only cached native control, and KMP. Each uses the actual view delegate method and matched inputs. Medians across three fresh processes per configuration, in nanoseconds per warm call:
Caching explains much of the improvement over the original NSNumber loop. The cached native control is faster than KMP in every configuration. KMP's first property assignment measured 130–207 µs versus 1.2–3.6 µs for cached native; its first fling measured 14.5–27.6 µs. The isolated KMP executable was 832,432 bytes larger than the original native executable, but includes the runtime and all three shared use cases. This is neither an incremental scroll-only size nor an installed application size.
These optimized ARM64 simulator probes measure method calls, not frame timing, physical gestures or device performance. Common-source ownership and shared regression coverage must justify the added runtime/build burden; the results do not establish a performance reason to adopt KMP.
Changelog:
[GENERAL] [CHANGED] - Share explicit scroll snap-offset target selection on Android and opt-in KMP iOS source builds.
Test Plan
The scroll implementation is byte-identical to reviewed head
9cc8379; this restack only inherits the gradient-layer hosted-runtime fix. The inherited multipart graph passes 112 configurations on each of CocoaPods 1.14.2 and 1.15.2. Fresh focused runtime checks are recorded in the prerequisite PRs.Runtime-ownership coverage includes both nested and sibling hosted XCTest bundles. Hosted tests use the application’s Kotlin runtime, while standalone tests keep their own runtime. The retained three-use-case library/static-framework runs at
9cc8379each passed 154 active tests under Address Sanitizer and launched successfully.From
packages/react-native/ReactShared:./gradlew jvmTest iosSimulatorArm64Test --max-workers=2 python3 scripts/test-apple-scroll-snap.py --benchmark-repeats 0 # Omit --benchmark-repeats 0 for the three-implementation cost probe.The full ReactAndroid suite was run from the repository with:
All 38 common tests pass on both JVM and ARM64 iOS simulator, including six selector tests; all six Debug/Release iOS device and simulator frameworks build, including Intel simulator slices. Nine checked-in Android regressions pass. The regressions call the real three views' fling paths and cover velocity/direction, free ranges, ties, axes/RTL, mutable lists, empty lists, endpoints and integer overflow.
An additional Robolectric differential fixture compiles the full original and changed Android view classes and passes 80,640 target/velocity/overscroll comparisons with controlled prediction and drawing. These are separate from the 18 ART assertions across API 24/26/37. Its two local baseline tests are not included in the nine checked-in tests.
The external direct-fling fixture initially reproduced the same nested-scroll bookkeeping issue with both native and KMP AARs. Initializing that state through AndroidX’s public API made the unchanged 80/96 assertions pass for both; no production code or expected target was changed. This fixture covers zero-velocity LTR snapping and retained-list updates, not general gesture/RTL physics.
Actual Apple native/KMP view comparison: 30,282 cases passed. The cached native control independently passed the same 30,282 comparisons. Catalyst compiles without Kotlin references.
Cost probe: 27 fresh processes across three implementations, three list sizes and three repeats; all checksums agree. No other task-owned benchmark/build ran during the measured window. Source hashes identify the combined shared framework used.
The assembled stack passed the full ReactAndroid suite: 623 tests across 94 suites, zero failures/errors/skips; RNTester ARM64 Release and 16 KiB alignment passed.
Previously validated local Maven consumers and packed-npm source consumers passed Debug and R8 Release. Source consumption builds standard Hermes from source. Five AARs contain each of the 12 shared class entries exactly once, and bytecode checks confirm actual calls from all three use cases.
Both Release consumers passed real gradient and multipart assertions on ARM64 API 24, 26 and 37. A separate R8 fixture passed six explicit-offset assertions per API using the real vertical, horizontal and nested views, including retained-list updates. RNTester exercised ten gradient routes and three normal scroll routes on each API 26/37, plus two gradient routes on API 24. These are targeted assertions and UI smoke, not exhaustive gestures or pixel comparisons.
Retained static-library and static-framework RNTester runs at
9cc8379each passed 154 tests, with 16 upstream skips and zero failures under Address Sanitizer, and launched. These repeat the same suite, not 308 distinct tests. Compiled adapters call all three shared APIs. The application owns one runtime; hosted integration tests import it, while standalone unit tests own their runtime separately.Universal Catalyst source build and actual ARM64/Rosetta x86_64 launches passed representative linear/radial rendering checks. All six compiled adapter objects use native fallback, and the final binaries have no shared Kotlin runtime. This is Catalyst fallback coverage, not Intel iOS simulator execution.
Four independent Kotlin runtime/static-dynamic combinations passed. An empty Objective-C owner linked with
-ObjCretains all shared APIs. A separate licensed XCFramework/SwiftPM distribution consumer builds for device/simulator/Catalyst and passes one ARM64 simulator test, reusing the matching Release framework archives. This does not enable complete React Native core SwiftPM/prebuilt adoption.The final assembled package includes all 44 shared-module files and excludes shared generated build outputs and caches.
Retained unsigned iOS ARM64 Release source build at
9cc8379passed. All three actual adapter objects call their shared APIs, and the app executable owns one Kotlin runtime. No physical-device execution is claimed.Dynamic-framework RNTester on ARM64 iOS simulator passed 154 tests, with 16 upstream skips and no failures, and launched. The actual compiled scroll object references
RNSScrollSnapOffsets, alongside the gradient and multipart adapters; React.framework owns the runtime once in the app load graph.The cost run used
1d628d8e0d6; Android application validation used586f67bafb46plus a multipart test annotation. Their production algorithms, adapters and relevant build inputs match this layer. Retained Apple static-library, static-framework, unsigned device and Catalyst validation uses9cc8379d0ad8. Updated hosted-test resolution comes from the gradient prerequisite; the scroll implementation and adapters remain unchanged. Dynamic execution is retained from586f67bafb46: the changed CocoaPods hosted-test logic is outside its unchanged dynamic path, the dynamic runtime owner is unchanged, and the preserved binaries passed the ownership guard. It is not a new final-commit dynamic build or run. Common/framework validation at265de92and separate distribution/runtime checks at772c54bhave unchanged production inputs. These app tests cover the combined stack, rather than a scroll-only application.Intel simulator execution remains deferred, and physical-device execution is unavailable. Full React Native core SwiftPM/prebuilt adoption is still guarded. Correctness/integration passes do not establish an accepted product performance budget.