Skip to content

feat: migrate android implementation to rive compose api - #120

Open
osama-mo wants to merge 1 commit into
muazkadan:mainfrom
osama-mo:feat/android-compose-migration
Open

osama-mo wants to merge 1 commit into
muazkadan:mainfrom
osama-mo:feat/android-compose-migration

Conversation

@osama-mo

@osama-mo osama-mo commented Mar 7, 2026

Copy link
Copy Markdown

Description

This PR migrates the Android implementation of Rive-CMP from the legacy imperative RiveAnimationView to the modern Jetpack Compose native RiveUI API provided by app.rive:rive-android:10.5.0.

This migration aligns the library with the official Rive Android Compose API documentation. These changes modernize the rendering pipeline, deprecate old state machine inputs in favor of ViewModels, and resolve several native C++ libEGL context crashes related to LazyColumn recycling.

Changes Made

  • API Migration: Replaced the AndroidView wrapper around RiveAnimationView with the new Compose native RiveUI composable.
  • Async Byte Loading: Since the new Compose runtime doesn't directly support URL paths natively, we now fetch the bytes asynchronously via withContext(Dispatchers.IO) and pass them to RiveFileSource.Bytes.
  • State Machine Inputs to ViewModels: Refactored RiveComposition to use ViewModelInstance and fireTrigger() since the legacy #trigger() method is deprecated in favor of View Models.
  • LRU Caching: Added an android.util.LruCache to cache downloaded Rive byte arrays. This drastically reduces network overhead and lag when scrolling through lists of animations.
  • LazyColumn EGL Crash Fixes:
    • Deferred the initialization of ViewModelInstance unless explicitly required, alleviating pressure on the native C++ engine.
    • Updated the Sample app (App.kt) to persist CustomRiveAnimation in the view hierarchy using Modifier.graphicsLayer { alpha = ... } instead of conditional if (visible) blocks during swipe gestures. This prevents Compose from continuously creating/destroying the native OpenGL Contexts and completely eliminates the std::__ndk1::mutex::lock()+12 (SIGSEGV) runtime crashes during rapid recompositions.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request modernizes the Android implementation of Rive-CMP by migrating from the legacy RiveAnimationView to the native Jetpack Compose RiveUI API. This transition enhances performance, aligns with current best practices, and resolves critical native C++ rendering issues, particularly those encountered with LazyColumn recycling.

Highlights

  • API Migration: The legacy imperative RiveAnimationView wrapped in AndroidView was replaced with the modern Jetpack Compose native RiveUI API.
  • Asynchronous Byte Loading: Rive files from URLs are now fetched asynchronously using withContext(Dispatchers.IO) and passed as RiveFileSource.Bytes, as the new Compose runtime does not directly support URL paths.
  • State Machine Input Refactoring: RiveComposition was refactored to utilize ViewModelInstance and fireTrigger() for state machine interactions, deprecating the older #trigger() method.
  • LRU Caching Implementation: An android.util.LruCache was added to cache downloaded Rive byte arrays, significantly reducing network overhead and improving performance in lists.
  • LazyColumn EGL Crash Resolution: Initialization of ViewModelInstance was deferred, and the sample app was updated to use Modifier.graphicsLayer { alpha = ... } for persisting CustomRiveAnimation in the view hierarchy, preventing native C++ libEGL context crashes during rapid recompositions.
Changelog
  • library/src/androidMain/kotlin/dev/muazkadan/rivecmp/CustomRiveAnimation.android.kt
    • Replaced AndroidView with RiveUI composable for rendering Rive animations.
    • Introduced internal composables CustomRiveAnimationInternal to handle both URL and byte array sources.
    • Implemented asynchronous loading of Rive files from URLs using Dispatchers.IO and LruCache.
    • Integrated ViewModelInstance for managing Rive state machine interactions.
    • Updated imports to reflect the new app.rive Compose API.
  • library/src/androidMain/kotlin/dev/muazkadan/rivecmp/RiveComposition.android.kt
    • Migrated internal references from RiveAnimationView to ViewModelInstance.
    • Updated setNumberInput, setBooleanInput, and setTriggerInput methods to use the new ViewModelInstance API.
    • Added comments indicating that pause, reset, and stop methods are handled differently or are no-ops in the new API.
  • sample/src/commonMain/kotlin/dev/muazkadan/rivecmpdemo/App.kt
    • Added graphicsLayer import.
    • Modified CustomPullRefreshSample to apply Modifier.graphicsLayer { alpha = ... } to the Rive animation container, ensuring it remains in the view hierarchy.
    • Modified ListItemUI to apply Modifier.graphicsLayer { alpha = ... } to the Rive animation container, preventing its continuous creation/destruction.
Activity
  • No specific activity (comments, reviews, progress) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request migrates the Android implementation to the modern Rive Compose API, aligning the library with current best practices, improving performance with caching, and resolving critical native crashes. However, this migration introduces a potential security vulnerability related to how remote Rive files are fetched. The review also focuses on refining the new API surface, improving error handling, and ensuring consistency across the common and platform-specific code to make the API less error-prone and more maintainable.

Comment on lines +151 to +159
RiveUI(
file = actualFile,
modifier = modifier,
artboard = artboard,
stateMachineName = stateMachineName,
viewModelInstance = vmi,
fit = fit.toAndroidFit(),
alignment = alignment.toAndroidAlignment()
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There are a couple of issues with parameter handling here:

  1. The autoPlay parameter is not passed to the RiveUI composable. This means the caller's input is ignored, and the animation will use RiveUI's default autoplay value (true). You should pass the autoPlay parameter through.

  2. The overlay parameter is a remnant from the old API and is not used by the new RiveUI composable. It should be removed from all CustomRiveAnimation function signatures (including the expect declarations in common code) to avoid confusion.

            RiveUI(
                file = actualFile,
                modifier = modifier,
                artboard = artboard,
                stateMachineName = stateMachineName,
                viewModelInstance = vmi,
                autoplay = autoPlay,
                fit = fit.toAndroidFit(),
                alignment = alignment.toAndroidAlignment()
            )

Comment on lines 11 to 13
actual fun setNumberInput(stateMachineName: String, name: String, value: Float) {
animationViewRef?.setNumberState(
stateMachineName = stateMachineName,
inputName = name,
value = value
)
viewModelInstanceRef?.setNumber(name, value)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The stateMachineName parameter is unused here, and also in setBooleanInput and setTriggerInput. The ViewModelInstance is already scoped to a specific state machine at creation time, making this parameter redundant in the Android implementation.

To create a consistent and clear API, this parameter should be removed from these methods in the common expect class RiveComposition and all its actual implementations.

Suggested change
actual fun setNumberInput(stateMachineName: String, name: String, value: Float) {
animationViewRef?.setNumberState(
stateMachineName = stateMachineName,
inputName = name,
value = value
)
viewModelInstanceRef?.setNumber(name, value)
}
actual fun setNumberInput(name: String, value: Float) {
viewModelInstanceRef?.setNumber(name, value)
}

if (value == null) {
value = withContext(Dispatchers.IO) {
try {
val downloaded = URL(url).readBytes()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The CustomRiveAnimationInternal function uses URL(url).readBytes() to fetch animation data from a provided URL. This URL is passed directly from the public CustomRiveAnimation composable without any validation or sanitization. An attacker could provide a malicious URL using schemes like file:// to read sensitive local files from the application's data directory or assets, or use http:// to perform requests to internal network resources (SSRF). While this is a client-side library, the lack of URL validation can lead to local file inclusion or unauthorized network requests if the application allows user-controlled URLs to be passed to this component.

}

@ExperimentalRiveCmpApi
private val riveByteCache = LruCache<String, ByteArray>(20)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The cache size 20 is a magic number. It's better to define it as a named constant to improve readability and make it easier to change in the future. Please define a constant for this value and use it here.

Suggested change
private val riveByteCache = LruCache<String, ByteArray>(20)
private val riveByteCache = LruCache<String, ByteArray>(RIVE_BYTE_CACHE_SIZE)

Comment on lines +96 to +98
} catch (e: Exception) {
null
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Catching a generic Exception is too broad and can hide unexpected runtime errors. It's better to catch more specific exceptions, like java.io.IOException, to handle network or file-related errors explicitly. Additionally, logging the exception would be very helpful for debugging cases where an animation fails to load.

                } catch (e: java.io.IOException) {
                    // Consider logging the exception for easier debugging, e.g., using android.util.Log
                    null
                }

Comment on lines +161 to 163
else -> {
// Wait while loading or handle failure silently
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Silently ignoring loading failures makes it very difficult to diagnose problems, such as a corrupt Rive file or other loading errors. While you might not want to show an error UI, you should at least log the failure case. This will be invaluable for debugging.

        else -> {
            if (result is Result.Failure) {
                // Log the error to help with debugging, e.g., using android.util.Log
            }
            // Wait while loading or handle failure silently 
        }

@muazkadan muazkadan added the invalid This doesn't seem right label Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid This doesn't seem right

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants