rive-react-native icon indicating copy to clipboard operation
rive-react-native copied to clipboard

[Feature] Support loading Rive animations from Expo Assets

Open nderscore opened this issue 1 year ago • 4 comments

This is a follow-up to #123 and #185 - please refer to these issues for additional context.


Description

This issue seeks to add support for Expo Assets to the React Native Rive integration.

Expo Assets enables developers to load custom assets asynchronously without needing to explicitly bundle them into the native app code. This enables features like hot-reloading during local development, as well as the ability to deploy updates through EAS update

Without this, there is a lot of friction that discourages the use of Rive on an Expo project:

  • During local development, you can't hot-reload Rive animations. You need to do a full build of your custom dev client.

  • After deployment, it is not possible to update the Rive animations embedded in the app without a new native build being published to the app store. There is no way to update the animations over EAS Update.

Current behavior

On iOS, it is already possible to use Expo Assets using the instructions found in the initial issue (#123)

On Android, this does not work. The Rive adaptor chokes when it receives a file:// URL.

Expected behavior

I can pass in a file:// URL from Expo Asset into the Rive adapter for React Native, and it works on all platforms.

nderscore avatar May 15 '24 21:05 nderscore

We are considering using Rive in our Expo app. Our current two options are

a) Download the rive assets with urls, which isn't a great user experience especially on startup. b) Do a new dev build every time we update something (5-6 minutes), which is not a great developer experience.

Neither option seems that great.

This feature seems critical if you are going to get any wider adoption in react-native.

kevin-ashton avatar Jun 10 '24 19:06 kevin-ashton

Glad I found this issue. Thank you. Will be sticking with Lottie

D1no avatar Jul 18 '24 00:07 D1no

I finally have a solution. Hope this makes its way into Rive eventually...but it requires 2 steps:

  1. edit your metro config so that it knows to bundle *.riv files:
config.resolver["assetExts"] = [
  ...(config.resolver.assetExts || []),
  // for rive animations
  "riv",
];
  1. Write a wrapper around the default Rive component in order to convert the required asset:
import React, { forwardRef } from 'react';
import Rive, { RiveRef } from "rive-react-native";
// @ts-ignore
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';

type RiveComponentProps = Omit<React.ComponentProps<typeof Rive>, 'url' | 'resourceName'> & {
  source: any
}

// detects if it is an http or file url
const stringIsUrl = (uri: string) => {
  return uri.startsWith('http') || uri.startsWith('file');
}

export const RiveAnimation = forwardRef<RiveRef, RiveComponentProps>(
  (props, ref) => {

    const { source, ...riveProps } = props;
    const resolved = resolveAssetSource(source);
    const uriIsUrl = stringIsUrl(resolved.uri);

    return (
      <Rive
        ref={ref}
        {...riveProps}
        resourceName={!uriIsUrl ? resolved.uri : undefined}
        url={uriIsUrl ? resolved.uri : undefined}
      />
    );
  }
);

This works just like images in expo and react-native. In development, the file is served by metro, so it's super fast and dynamic to change images. In production, the asset is bundled and the appropriate local uri is handled for you. It just works. I just tested in both environments and it works great. SOOOO much better than every other solution I've seen here on github.

*note that this is edited. The original code was missing the resourceName prop for bundled Android apps and was only working in dev mode

tslater avatar Aug 31 '24 07:08 tslater

Hey @tslater

Thanks a lot for the code snippet! We implemented the same and it works great on IOS. On Android though it only works in development mode ; not in production. Does this work for you?

anatoleblanc avatar Oct 21 '24 09:10 anatoleblanc

Thanks @tslater for the code 👍

It's seems code crash when I used an external URL in the source property of <RiveAnimation />.

This version works for me on iOS (simu) and Android (emu):

import React, { forwardRef, useMemo } from "react";
import Rive, { RiveRef } from "rive-react-native";
// @ts-ignore
import resolveAssetSource from "react-native/Libraries/Image/resolveAssetSource";

type RiveComponentProps = Omit<
  React.ComponentProps<typeof Rive>,
  "url" | "resourceName"
> & {
  source: string | number;
};

const isValidUrl = (uri: string | undefined): boolean => {
  if (!uri) return false;
  return uri.startsWith("http") || uri.startsWith("file");
};

export const RiveAnimation = forwardRef<RiveRef, RiveComponentProps>(
  (props, ref) => {
    const { source, ...riveProps } = props;

    const riveConfig = useMemo(() => {
      if (typeof source === "string" && isValidUrl(source)) {
        return { url: source };
      }

      const resolved = resolveAssetSource(source);
      const uri = resolved?.uri;
      const isUrl = isValidUrl(uri);

      return {
        resourceName: !isUrl && uri ? uri : undefined,
        url: isUrl ? uri : undefined,
      };
    }, [source]);

    return <Rive ref={ref} {...riveProps} {...riveConfig} />;
  }
);

Then I can use these three types of source:

// Import
import truckV7 from "../assets/animations/truck_v7.riv";
// ...
<RiveAnimation source={truckV7} />;
// Require
<RiveAnimation source={require("../assets/animations/truck_v7.riv")} />
// Remote URL
<RiveAnimation source={"https://public.uat.rive.app/community/runtime-files/148-325-tape.riv"} />

Also, when .riv files are loaded locally like that, It looks like there is a graphic white blink on iOS (simu) when animationName is set but maybe it's just my implementation that's incorrect.

https://github.com/user-attachments/assets/14475d20-5cf3-4ef5-b494-f65950060ce6

guval-gh avatar Nov 07 '24 01:11 guval-gh

@guval-gh If you look at the react native docs, when you use a remote url, you should use the url prop.
Link to the docs: https://help.rive.app/runtimes/overview/react-native#id-3.-add-the-rive-component Here's the example:

import Rive from 'rive-react-native';

function App() {
  return <Rive
      url="https://public.rive.app/community/runtime-files/2195-4346-avatar-pack-use-case.riv"
      artboardName="Avatar 1"
      stateMachineName="avatar"
      style={{width: 400, height: 400}}
  />;
}

Can you try that and let me know if it works or not?

tslater avatar Nov 07 '24 19:11 tslater

@tslater Yes, when we use the classical <Rive />component, we can use url property. But I spoke about your custom component <RiveAnimation />. If I would like to use it and pass to the source custom property (an import, require or remote url), I needed to modify the <RiveAnimation /> code a bit.

guval-gh avatar Nov 07 '24 20:11 guval-gh

@guval-gh I'm unsure of what you mean. Have you tried using the url prop with the wrapped component code I provided? I forwards all the props, so it should work. What error or behavior do you see if you use the url prop?

Did you get it working with modifications? Do you want to share those?

tslater avatar Nov 08 '24 01:11 tslater

@tslater I mean, of course I can use the url props but, if I use the custom <RiveAnimation /> component dynamically, I need that the custom property source works no matter what.

With your code, if the source pass is a remote url, the code crash. So I just modify it a little bit to handle the three possibilities (source as import, require or remote url).

Yes, it works, I share it just above https://github.com/rive-app/rive-react-native/issues/241#issuecomment-2461128365

guval-gh avatar Nov 08 '24 01:11 guval-gh

this is actually awesome, thank you both @guval-gh and @tslater for the original idea 🙌🏼 Would say this should be part of the official docs in a bit more organised way 🙏🏼

MarcoEscaleira avatar Jan 28 '25 19:01 MarcoEscaleira

I don't believe the above solutions work on Android Production builds even with the resourceName prop, however the local asset runs fine via debug / developer mode on device. iOS working great. Having to revert to remote URLs to support Android for now though

josh-deprogram avatar Jan 29 '25 07:01 josh-deprogram

Thanks for your reply @josh-deprogram.

I created a "rive playground" (https://github.com/guval-gh/rive-playground) to make some tests with React Native/Rive and keep tracking solutions to handle .riv assets.

Also, I think the solution 1 (with expo-custom-assets https://github.com/guval-gh/rive-playground?tab=readme-ov-file#handle-riv-assets-with-expo-custom-assets) has a better compatibility for the moment (even if it's less practical, as it requires rebuilding).

I don't necessarily have the time right now, but I'll try to test each solution on both platforms with production builds and improve the repo.

guval-gh avatar Jan 29 '25 08:01 guval-gh

Ok thanks, I will try that implementation for now as loading local would be much preferred. The solution of the Rive wrapped component has potential to be awesome so I will try a few things also when more time, keen to see the outcome of that

josh-deprogram avatar Jan 29 '25 10:01 josh-deprogram

@josh-deprogram we're using the solution on over 500k android devices in production. It should work. Can you tell us more about what isn't working?

tslater avatar Jan 29 '25 10:01 tslater

@tslater On device (Pixel6) in prod mode the asset fails to resolve with the wrapper component and giving the resourceName. I can see that the onError is outputting the following: Unable to download Rive file from: file:///data/user/0/com.kontrak.app/files/.expo-internal/2f42b23e141c821f7477999f26ed5a18.riv

I have verified that this file looks to be bundled in the assets via expo-updates, rive-react-native is "8.3.0". Remote URLs function as expected. iOS functions as expected

josh-deprogram avatar Jan 29 '25 11:01 josh-deprogram

@josh-deprogram Thanks for sharing that. Can you also tell me what version of ReactNative and Expo (if you're using Expo) you are on? Also, can you share the code for the parent of <RiveAnimation />? (or whatever you named your wrapper for asset management)?

tslater avatar Apr 10 '25 18:04 tslater

This is the error provided to handleURLAssetError, which causes the "Unable to download Rive file from: file:///data/user/0/…" message:

com.android.volley.VolleyError: java.lang.ClassCastException: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection
	at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:164)
	at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
	at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
Caused by: java.lang.ClassCastException: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection
	at com.android.volley.toolbox.HurlStack.createConnection(HurlStack.java:197)
	at com.android.volley.toolbox.HurlStack.openConnection(HurlStack.java:215)
	at com.android.volley.toolbox.HurlStack.executeRequest(HurlStack.java:83)
	at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:104)
	at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:132)
	at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111) 
	at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)

Update: this is occurring with rive-react-native v9.3.0

ckknight avatar May 14 '25 19:05 ckknight

Fix for the "Unable to download Rive file from: file:///data/user/0/…" error: #329

ckknight avatar May 14 '25 20:05 ckknight

Thanks @ckknight ! that did the trick! For the sake of simplicity, I just did:

    if(url.startsWith("file://")){
      // read from file directly
      try {
        // Extract file path from file:// URL
        val filePath = url.substring(7) // Remove "file://"
        val file = java.io.File(filePath)

        // Read file directly
        val data = file.readBytes()
        listener.onResponse(data)
      } catch (e: Exception) {
        // Pretend the error came from Volley, which is how http URLs are loaded
        handleURLAssetError(url, VolleyError(e), isUserHandlingErrors)
      }

    } else {
      // fetch with Volley
      val queue = Volley.newRequestQueue(context)

      val stringRequest = RNRiveFileRequest(
        url, listener
      ) { error -> handleURLAssetError(url, error, isUserHandlingErrors) }

      queue.add(stringRequest)
    }

diegodorado avatar Jun 18 '25 14:06 diegodorado

we're having the same issue on iOS. it works in debug mode but not the release mode. @ckknight do you have any idea why would it be broken in the release mode?

AlirezaHadjar avatar Jun 18 '25 21:06 AlirezaHadjar

Loading .riv files from file:/// URLs is not working on iOS for me on a real device in 9.3.4

UIApplication.shared.canOpenURL(url)

This return false for a file:/// URL. This behavior was possibly introduced in a recent change.

Making this change fixed it:

    private func isValidUrl(_ url: String) -> Bool {
        if let url = URL(string: url) {
            return url.isFileURL ? true : UIApplication.shared.canOpenURL(url)
        } else {
            return false
        }
    }

But it should probably just short-circuit and return Data more directly for a file:///.

zhm avatar Jun 23 '25 20:06 zhm

Second this... it's a huge blocker since riv file can be a few MB in size. Downloading through internet might not even work for offline first apps.

oeddyo avatar Aug 23 '25 21:08 oeddyo