rive-app / rive-react-native

MIT License
551 stars 43 forks source link

Expo: Automatically add assets to platforms #185

Open MortadhaFadhlaoui opened 1 year ago

MortadhaFadhlaoui commented 1 year ago

Description

I'm using rive-react-native in the expo dev build and it's working perfectly for IOS but I have a blank page on Android

Device & Versions (please complete the following information)

Additional context

The rive file output after using useAssets is

LOG {"url": "file:///data/user/0/com.aumio.app.dev/cache/ExponentAsset-178c3de6e80c005b7cdc90b52c58f947.riv"}

This is the source code:

const RIVE_ANIMATION: riveAnimation[] = [
  {
    name: 'downloadAnimation',
    url: require('src/assets/animations/downloadAnimation.riv'),
  },
];

export const useRiveAnimation = () => {
  const animations = RIVE_ANIMATION.map((animation) => animation.url);
  const [assets, error] = useAssets(animations);
  if (error) return;
  return assets?.[0]?.localUri;
};

  const url = useRiveAnimation();

      {url && (
        <Rive
          ref={riveRef}
          url={url}
          autoplay={false}
          stateMachineName="downloadAnimation"
          onStateChanged={(stateMachineName, stateName) => {
            console.log(
              'onStateChanged: ',
              'stateMachineName: ',
              stateMachineName,
              'stateName: ',
              stateName,
            );
          }}
        />
      )}

app.config.js

  plugins: [
...
 [
        'expo-build-properties',
        {
          android: {
            minSdkVersion: 21,
            compileSdkVersion: 33,
            targetSdkVersion: 33,
            buildToolsVersion: '33.0.1',
            kotlinVersion: '1.8.10',
          },
          ios: {
            deploymentTarget: '14.0',
            useFrameworks: 'static',
          },
        },
      ],
      ...
      ]
HayesGordon commented 1 year ago

Hi @MortadhaFadhlaoui, could you please provide us with an easy to reproduce example. Either as a zip or a repository that includes a .riv that reproduces the issue.

This will help us look into your issue quicker, thanks!

MortadhaFadhlaoui commented 1 year ago

Hi @MortadhaFadhlaoui, could you please provide us with an easy-to-reproduce example. Either as a zip or a repository that includes a .riv that reproduces the issue.

This will help us look into your issue quicker, thanks!

Thank you very much for checking out with me

here you find the repo.

HayesGordon commented 1 year ago

@MortadhaFadhlaoui I see you're using the url parameter to pass in the local resource URI for the expo asset.

Note that the url parameter is intended for loading Rive assets over the internet. It seems though that the iOS runtime just so happens to play nicely with local resources as well, but our Android runtime does not. This is something that we could look into improving on Android's side, but I can't guarantee that this will be added or would be possible.

Note that for local resources we recommend using the resourceName parameter, which requires you to add the resources to the Android and iOS folders (which is the current process for a normal React Native app). I'm not very familiar with Expo, so I'm not sure if there is an alternative way to add resources than how you're doing it.

But to get your Rive animation working you will need to add the resource to: android->app->src->main->res->raw->resource_name.riv

On Android you cannot use capital letters for these resource files.

On iOS you need to open the project in Xcode and drag in the file to any folder and be sure to link it to the application.

Then you can use the resourceName parameter (note it does not require the .riv extension):

<Rive
  style={{ backgroundColor: "red", flex: 1, margin: 20 }}
  ref={riveRef}
  resourceName="download_animation"
  autoplay={false}
  stateMachineName="downloadAnimation"
  onStateChanged={(stateMachineName, stateName) => {
    console.log(
      "onStateChanged: ",
      "stateMachineName: ",
      stateMachineName,
      "stateName: ",
      stateName
    );
  }}
/>
MortadhaFadhlaoui commented 1 year ago

Sounds good man thank you for explaining and helping me, I create a quick plugin that worked perfectly for Android to add assets as a resource under raw 🚀.

const { withDangerousMod } = require("@expo/config-plugins");
const fs = require("fs-extra");
const path = require("path");

const withCustomAssets = (config) => {
  config = modifyResourcesAndroid(config);
  return config;
};

function modifyResourcesAndroid(config) {
  // Specify the source directory of your assets
  const assetSourceDir = "assets/riv";

  return withDangerousMod(config, [
    "android",
    async (config) => {
      // Get the path to the Android project directory
      const projectRoot = config.modRequest.projectRoot;

      // Get the path to the Android resources directory
      const resDir = path.join(
        projectRoot,
        "android",
        "app",
        "src",
        "main",
        "res"
      );

      // Create the 'raw' directory if it doesn't exist
      const rawDir = path.join(resDir, "raw");
      fs.ensureDirSync(rawDir);

      // Get the path to the assets directory
      const assetSourcePath = path.join(projectRoot, assetSourceDir);

      // Retrieve all files in the assets directory
      const assetFiles = await fs.readdir(assetSourcePath);

      // Move each asset file to the resources 'raw' directory
      for (const assetFile of assetFiles) {
        const srcAssetPath = path.join(assetSourcePath, assetFile);
        const destAssetPath = path.join(rawDir, assetFile);
        fs.copyFileSync(srcAssetPath, destAssetPath);
      }

      // Update the Android resources XML file
      const resourcesXmlPath = path.join(resDir, "values", "resources.xml");

      let resourcesXml;
      if (fs.existsSync(resourcesXmlPath)) {
        resourcesXml = await fs.readFile(resourcesXmlPath, "utf-8");
      } else {
        resourcesXml = "<resources>\n</resources>";
      }

      const rawResourcesTags = assetFiles.map(
        (assetFile) =>
          `<string name="${assetFile.substring(
            0,
            assetFile.lastIndexOf(".")
          )}">${assetFile}</string>`
      );

      const rawResourcesRegex = /<string name="[^"]+">[^<]+<\/string>/;

      for (const rawResourcesTag of rawResourcesTags) {
        if (resourcesXml.match(rawResourcesRegex)) {
          resourcesXml = resourcesXml.replace(
            rawResourcesRegex,
            rawResourcesTag
          );
        } else {
          const stringResourcesRegex = /<\/resources>/;
          resourcesXml = resourcesXml.replace(
            stringResourcesRegex,
            `${rawResourcesTag}\n    </resources>`
          );
        }
      }

      await fs.writeFile(resourcesXmlPath, resourcesXml);

      return config;
    },
  ]);
}

module.exports = withCustomAssets;
HayesGordon commented 1 year ago

Awesome 👍

I'm going to rename the issue for better discoverability and leave it open until we have a better solution.

HayesGordon commented 1 year ago

@MortadhaFadhlaoui if it's not too much trouble, do you mind updating the repository you made with your plugin you made here. So that someone can easily find a working example

MortadhaFadhlaoui commented 1 year ago

Thank you again, repo updated

CoreyBovalina commented 11 months ago

@MortadhaFadhlaoui i am trying to implement your solution and when referencing the plugin in the app.json file, it does not seem to want to recognize it.

update: I was able to get this to work, but I had to make the ContentAssetPlugin file .js and not .ts

ridwansameer commented 11 months ago

Might be worth the Rive team's time to implement a official config plugin for expo, would be useful and bring rive to a larger audience (More and more people are using an Expo Managed flow)

https://docs.expo.dev/config-plugins/introduction/ For reference

Also might be worth copying over the resources to iOS as well since iOS loading the local resource seems slightly unexpected.

uffoltzl commented 10 months ago

I have completed what @MortadhaFadhlaoui made by adding the iOS version here. Would be happy to implement it officially if interested

mzaien commented 9 months ago

Hello everyone, I’ve made a plugin for both os’s https://github.com/Malaa-tech/expo-custom-assets

it used @MortadhaFadhlaoui code for anroid with small change to add multiple files

if this can be built in rive-react-native i would love to do so

daehyeonmun2021 commented 8 months ago

@mzaien 's plugin works like magic. Thanks for the community !

mzaien commented 8 months ago

@mzaien 's plugin works like magic. Thanks for the community !

It is my pleasure to help

BrandonYuen commented 7 months ago

Plugin works great @mzaien thank you! I wonder if it's possible to make this work with hot-reloading when developing with a development client? So that we don't have to rebuild the development client every time we make changes or add new .riv files to the assets.

mzaien commented 7 months ago

Plugin works great @mzaien thank you! I wonder if it's possible to make this work with hot-reloading when developing with a development client? So that we don't have to rebuild the development client every time we make changes or add new .riv files to the assets.

I would love to have this, but to my knowledge to run a config plugin you have to do a prebuild if you know something that can help me to do it on hot reload I will try to implement it

tslater commented 6 months ago

Just to summarize the state of things: @mzaien has a plugin that can do this, but it requires a new native build every time you add a new .riv asset, Is that correct?

Ideally we'd fix that issue, then also write that plugin into the rive RN package itself as a PR. Is that the work that needs to be done here?

bennycode commented 4 months ago

I also want to add Rive animations in my Expo app. From what I've discovered, there are three methods to achieve this:

1. Manually Adding Assets

Detailed in the "Loading in Rive Files" article, this method involves:

  1. Using Rive resourceName
  2. Manually adding resources using XCode and Android Studio

Advantages: ✅ Works for Android ✅ Works for iOS

Disadvantages: ❌ Requires XCode and Android Studio ❌ Requires managing native projects and their ios and android directories yourself (bare workflow)

2. Using expo-asset

As demonstrated in the "Using Rive in Expo" tutorial by @jellyninjadev (sample repository), this approach involves:

  1. Customizing the Metro bundler to resolve .riv files: config.resolver.assetExts.push("riv");
  2. Importing the useAssets hook from expo-asset to load Rive files
  3. Using Rive url with assets[0].localUri

Advantages: ✅ Works for iOS ✅ No manual resource copying ✅ Doesn't involve XCode or Android Studio ✅ Allows managed Expo workflow

Disadvantages:Rive url does not work with local resources when building for Android (see comment)

3. Using expo-custom-assets

This method involves using a config plugin named expo-custom-assets, which activates when running npx expo prebuild. Steps include:

  1. Using Rive resourceName
  2. Configuring the plugin in app.json to copy specified resources (installation guide)

Advantages: ✅ Works for Android ✅ Works for iOS ✅ No manual resource copying ✅ Doesn't involve XCode or Android Studio ✅ Allows managed Expo workflow

Disadvantages: N/A

Is this the current state of the art, or is there something I've missed?

tslater commented 2 months ago

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",
];

2) 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 commented 2 months ago

Doesn't "Using expo-custom-assets" require you to do new native build to add new assets? That's what turned me off. It's also an extra package. I believe there's a patch to support Android option 2, but it requires sending all of the data over the bridge.

I do think the solution I shared here is the simplest and least invasive. Also most consistent with how people are used to working with image or video assets in react-native or expo. I'd like to see it canonized and incorporated if others agree.

anatoleblanc commented 1 month ago

@tslater does it work on android in production? This works for us in dev mode on Android but not in prod! Thanks a lot!

bennycode commented 1 month ago

expo-custom-assets

Using expo-custom-assets works in prod. I have released https://play.google.com/store/apps/details?id=com.welovecoding.app with it! 🌟

If you tell me your issues, I can maybe help you setting it up.

anatoleblanc commented 1 month ago

Nice that expo-custom-assets is working in prod! If I understood well though, it does not provide hot reloading so we need to build a new app version every time we add an animation right?

I'm trying to combine the two approaches above to get both:

I'll revert back if I have any findings!

tslater commented 1 month ago

@anatoleblanc I forgot to update the issue, I did get it working on production with one more tweak, here is the final version that gives hotrealoding in dev and works in production:

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

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}
      />
    );
  }
);
anatoleblanc commented 1 month ago

Not sure you have copy pasted all the code, you do not seem to use Platform here?

tslater commented 1 month ago

@anatoleblanc Sorry, I tried to blend a custom version with something more generic. Either way, I was wrong and the version I posted earlier is actually edited to be the latest. It is working for us both in production and in development for iOS and Android, with hot reloading on both platforms as well.

Can you try logging the value of resolved on a production build to debug?

bennycode commented 1 week ago

I wrote an article for the Expo blog on integrating Rive animations in React Native apps: https://expo.dev/blog/how-to-add-an-animated-splash-screen-with-expo-custom-assets