Skip to content

Share gradient stop calculations with Kotlin Multiplatform - #3

Open
kunal26das wants to merge 2 commits into
codex/kmp-native-rntesterfrom
codex/kmp-native-gradients
Open

Share gradient stop calculations with Kotlin Multiplatform#3
kunal26das wants to merge 2 commits into
codex/kmp-native-rntesterfrom
codex/kmp-native-gradients

Conversation

@kunal26das

@kunal26das kunal26das commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

Architectural discussion: Discussion #1020 is the current venue for evaluating architectural fit and downstream costs. This fork stack is an experimental implementation reference; upstream KMP integration is awaiting architectural agreement.

Share CSS gradient stop positioning and transition-hint calculations between Android and iOS using the Kotlin Multiplatform foundation in upstream #58472. This is the first use case in a series; multipart framing upstream #58474 and explicit scroll snap-target selection upstream #58475 are separate proposals.

This native fork PR mirrors the upstream gradient calculations submission as layer 3 of six. This layer is based on RNTester dependencies. The registered fork stack uses dependent PR bases, so this PR shows its incremental diff.

Stack order: FoundationRNTester dependenciesGradientsNative multipart boundsShared multipart parsingScroll offsets.

This mirror retains the exact validated source of the upstream submission. The results below keep their original scope and provenance; restacking is not a new test run.

Android embeds the shared JVM code in its existing ReactAndroid AAR. iOS source builds opt in with RCT_USE_KMP=1 during CocoaPods installation. The common implementation takes stop positions and color-presence flags and returns positions, source color indices and interpolation weights. Native color interpolation, geometry, drawing and precision choices remain in the adapters. There is no Compose dependency or JavaScript API change.

This first consumer also establishes packaging and integration checks: actual Maven/npm consumers, shrinking, source applications, independent Kotlin framework coexistence, and repeatable cost probes. Catalyst keeps its native fallback. Full React Native core SwiftPM and published Apple prebuilt integration remain guarded; the separate XCFramework/SwiftPM consumer is a distribution experiment.

Feasibility and alternatives

The benefit is one Kotlin implementation and common tests for calculations otherwise maintained in Kotlin and Objective-C++. Android calls it directly without a new JNI interface. C++ is a strong alternative because the Apple adapter already uses C++ data types and React Native already ships a shared C++ runtime. This proposal does not establish that KMP is the preferred implementation.

The measured gradient bridge adds cost, including its conversions:

Probe Native → KMP
Five Apple simulator cases 2.5–43.2× latency; KMP 0.66–27.12 µs/call
Five Android emulator cases 1.58–2.96× latency; KMP 0.31–4.32 µs/call
Android allocated bytes Approximately 1.88–2.45×
Isolated Apple executable +687,736 bytes (~672 KiB)
Apple process footprint growth KMP 7.7–16.3 MiB in sampled runs

Each platform uses matched native/KMP inputs; hint inputs differ between platforms. These are simulator/emulator measurements, not app startup, frame time, installed size or physical-device results. Footprint snapshots are not allocation or leak measurements. Broader adoption needs an accepted performance budget and representative application/device evidence. Catalyst fallback also means the experiment does not eliminate every native copy.

Changelog:

[GENERAL] [CHANGED] - Share gradient stop calculations using Kotlin Multiplatform on Android and opt-in iOS source builds.

Test Plan

Base: upstream f109c0af8693b58e0716b77b0b7c0c2299fc0a0e. Local toolchain: JDK 17, Kotlin 2.4.20, Xcode 26.6/iOS 26.5. CI selects Xcode 26.4.1.

The common algorithms, native adapters and JVM packaging retain their previously validated implementations. Hosted-test runtime ownership is now established in this first consumer: full-pod sibling tests resolve shared classes through the actual application host, including projects without optional TestTargetID metadata. Standalone tests and tests hosted by a non-KMP app retain their own runtime. The application runner also rejects a second shared runtime in hosted test binaries.

Previously completed gradient-layer validation on unchanged algorithm and adapter sources, from packages/react-native/ReactShared:

./gradlew jvmTest iosSimulatorArm64Test exportAndroidJar --rerun-tasks --max-workers=2
./gradlew -PreactNativeSharedSmoke=true jvmTest iosSimulatorArm64Test --max-workers=2
./scripts/test-apple-smoke.sh
./scripts/test-apple-gradient.sh
RCT_KMP_BUILD_TYPE=Release ./scripts/test-apple-gradient.sh
  • 22 production common tests passed on each of JVM and ARM64 simulator. Opt-in smoke runs passed 26 each: the same 22 plus four compiler probes.
  • 521 real adapter parity cases passed in each Debug/Release × static/dynamic combination; Debug/Release Objective-C smoke passed five cases each.
  • Production JAR has no smoke class. npm dry-pack includes 34 shared-module files and excludes generated outputs/caches.

Hosted-runtime review checks executed at upstream revision 9e28d94bf595:

  • CocoaPods 1.14.2 and 1.15.2 each pass 104 generated configurations, with 208 per-SDK archive-link assertions plus host classification and repeated-hook checks. Cases include missing/stale metadata, custom app products, simulator/device host changes, and unrelated app dependencies. These are configuration checks, not 104 app builds.
  • The new binary guard rejects both retained gradient library/static products that contained a duplicate runtime in hosted integration tests.
  • Fresh full RNTester runs at that exact validated head pass in both static-library and static-framework modes: each executes 151 unit tests, with 16 upstream skips and zero failures under Address/Undefined Behavior Sanitizers, and launches successfully. These repeat the same suite. The app debug library is the sole shared-runtime owner; hosted IntegrationTests have no archive/runtime, while standalone UnitTests retain theirs. No duplicate RNS warnings or stale multipart/scroll classes occur. IntegrationTests has no enabled upstream test methods; the small fixtures below provide actual host/API-sharing test executions.
  • Exact-source small CocoaPods/Xcode projects pass five real XCTest cases in each static-library and static-framework mode on the ARM64 iOS simulator. The tests call the shared APIs and consume host-created objects. Metadata-present, metadata-free and SDK-dependent hosted bundles define no RNS classes; the app, standalone tests and tests hosted by a non-KMP app retain their expected runtime. Both modes have zero failures/skips or duplicate RNS warnings. Device/simulator host selection is checked against real Xcode settings; device execution is not claimed.
  • Architecture- or SDK-version-qualified host paths that cannot be resolved during pod installation remain a documented limitation; the helper does not treat them as proof of an app-provided runtime.

Previously executed on the identical production implementation, using the checked-in runners documented in ReactShared/README.md:

  • Android core: 611 tests/93 suites passed, including 12 gradient tests.
  • Three published and two packed-source AARs; fresh Maven and npm-source consumers passed Debug and R8 Release with artifact-provenance and class-uniqueness checks. Release adapter assertions passed on API 26 and 37.
  • ARM64 RNTester Release built; ten gradient routes exercised on each of API 26/37; representative rendering inspected; 16 KiB APK alignment passed.
  • All six Apple Debug/Release frameworks built, including Intel simulator slices.
  • Earlier gradient RNTester runs with libraries, static frameworks and dynamic frameworks each passed 151 tests, with 16 upstream skips and no failures. Review subsequently found duplicate shared classes in the library/static hosted test bundles despite those passes; these historical passes alone do not establish runtime ownership. The new artifact guard rejects both retained faulty products. Rendering checks were separate.
  • Unsigned iOS ARM64 Release and universal Catalyst builds passed; Catalyst's ARM64 app and native fallback were checked.
  • Independent Kotlin frameworks passed four static/dynamic ownership combinations, serial and concurrent calls.
  • Separate SwiftPM distribution consumer built for device, simulator and Catalyst; one ARM64 simulator test passed.
  • Shared build probes with warm dependencies/compiler and output cache disabled: clean 11.95 s, unchanged 0.53 s, source edit 7.91 s. Configuration-cache reuse and executed tests checked separately.

Intel simulator execution remains deferred; physical-device execution and complete React Native core SwiftPM/prebuilt adoption are outstanding. The mirrored upstream PR’s hosted CI needs upstream approval; these local passes do not claim that approval or completed CI on the new fork PR.

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.

1 participant