Skip to content

Support for SkipFuseUI Animation Performance Fix (Android) - #500

Open
tifroz wants to merge 10 commits into
skiptools:mainfrom
tifroz:experimental_animation-performance
Open

tifroz wants to merge 10 commits into
skiptools:mainfrom
tifroz:experimental_animation-performance

Conversation

@tifroz

@tifroz tifroz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Thank you for contributing to the Skip project! Please review the contribution guide at https://skip.dev/docs/contributing/ for advice and guidance on making high-quality PRs.

Use this space to describe your change and add any labels (bug, enhancement, documentation, etc.) to help categorize your contribution.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device
  • REQUIRED: I have checked whether this change requires a corresponding update in the Skip Fuse UI repository (see Animation Performance Fix (Android) skip-fuse-ui#134)
  • OPTIONAL: I have added an example of any UI changes to the Showcase sample app

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

Codex generated the code with supervision, UI tests conducted manually to validate performance improvements


Summary

Add Android rendering controls for expensive view subtrees, including the bridge support used by SkipFuseUI #134.

  • androidEquatable reuses evaluated content while its equality value remains unchanged.
  • androidCompositionBoundary retains a Compose host and its lifecycle while its ID stays stable. Changed input revisions update content without replacing the host, and parent constraints still control measurement.
  • Preserve ForEach child identity in stacks, including inner-row reordering in nested loops.
  • Cancel an in-flight animation when a plain state write changes its target; keep bridged provenance attached to its modifier.
  • Add combined compositor scale/translation, avoid a geometry-observation state write during composition, and avoid transient zero-sized placeholders for constrained resizable cached images.

Testing

After rebasing onto upstream/main, all 46 Android JUnit tests passed under Robolectric:

  • 10 AndroidEquatableTests: equality reuse, collection changes, lazy stacks, environment values, hoisted state, actions, and bridged factories.
  • 4 AndroidCompositionBoundaryTests: retained hosting, input updates, inherited environment, and remeasurement.
  • 9 AnimationTests and 18 TransactionTests: animation behavior and provenance.
  • 5 RenderingRegressionTests: animation isolation, content-sized hosting, and nested row identity in HStack, VStack, and ZStack.
skip test --filter RenderingRegressionTests --filter AnimationTests \
  --filter TransactionTests --filter AndroidEquatableTests \
  --filter AndroidCompositionBoundaryTests

The Swift build and test harness also completed successfully. The parity summary reports 41 matched cases; the five Kotlin-only regression tests are recorded separately in the JUnit reports. git diff --check passed.

Usage Example

For an Equatable view whose equality covers all body inputs:

ContactRow(contact: contact)
    .androidEquatable()

For a view that is not itself Equatable, supply an explicit key. Keep changing actions outside the cached subtree:

ContactLabel(contact: contact)
    .androidEquatable(recomposeOverride: contact)
    .onTapGesture { onSelect(contact) }

Here contact must be Equatable and contain every value used by ContactLabel's body. Include any additional body-affecting inputs in a combined key.

For independently retained hosting:

PlayerSurface(player: player)
    .androidCompositionBoundary(
        id: player.id.uuidString,
        inputs: String(player.renderRevision)
    )

API Shape

extension View where Self: Equatable {
    public func androidEquatable() -> some View
}

extension View {
    public func androidEquatable<RecomposeOverride: Equatable>(
        recomposeOverride: RecomposeOverride
    ) -> some View

    public func androidCompositionBoundary(
        id: String,
        inputs: String = ""
    ) -> some View

    public func compositorTransform(
        scaleX: CGFloat, scaleY: CGFloat,
        translationX: CGFloat, translationY: CGFloat,
        anchorX: CGFloat, anchorY: CGFloat
    ) -> any View
}

The bridge surface also adds lazy AndroidEquatableContent and AndroidCompositionBoundary projection lifecycle support.

Risks / Limitations

  • Equality keys must include every body-affecting input, including relevant environment and hoisted state values. State read inside the cached child's body does not automatically invalidate it.
  • Boundary callers must change inputs for parent-driven content changes. Changing id replaces the host and its retained state.
  • A boundary creates a separate Compose host and is heavier than equality reuse. It does not freeze layout size.
  • The new modifiers return the original view on non-Android platforms. compositorTransform changes rendered pixels without changing layout position.
  • The automated tests above verify behavior; they do not quantify performance improvements or validate physical-device animation quality.

Notes

SkipFuseUI #134 depends on this PR.

@cla-bot cla-bot Bot added the cla-signed label Jul 29, 2026
@tifroz tifroz changed the title Experimental animation performance Support for SkipFuseUI Animation Performance Fix (Android) Jul 29, 2026
@tifroz

tifroz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@marcprux pinging you for a code review (not urgent, but I think this may have fallen through the cracks)

@tifroz
tifroz force-pushed the experimental_animation-performance branch from 45b9111 to 3af2f36 Compare August 22, 2026 20:35
- preserve ForEach and stack composition identity
- improve equatable content caching and bridge equality
- support explicit first-render animation sources
- prevent animation provenance from leaking between modifiers
- document Android rendering performance APIs
- verify state updates refresh content without replacing the native host
- verify parent size changes remeasure the same host
- verify environment values remain available and update inside the boundary
- document that fixed sizing belongs outside the boundary
@tifroz
tifroz force-pushed the experimental_animation-performance branch from 3af2f36 to 96118ff Compare September 16, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant