rive-app / rive-react-native

MIT License
540 stars 39 forks source link

Working with Expo [docs] #123

Closed thejustinwalsh closed 5 months ago

thejustinwalsh commented 2 years ago

Description

Documentation for working with Expo is lacking a workflow for loading animations.

Details

When attempting to load an animation in expo I hit a wall relatively quickly regarding animation loading. Sharing my notes with anyone else who is using Rive in Expo.

Setup Metro Bundler

Add resolution for .riv files so that we may require references to the assets.

metro.config.js

const { getDefaultConfig } = require('expo/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.assetExts = [
  ...defaultConfig.resolver.assetExts,
  'riv'
];

module.exports = defaultConfig;

Use Expo Asset to load animations

Using the expo-asset module we can retrieve a full file URI to the animation that is bundled into our app, and load the animations using the url prop on the Rive component.

App.tsx

import { StatusBar } from 'expo-status-bar';
import { Image, StyleSheet, Text, View } from 'react-native';
import Rive, { Alignment, Fit } from 'rive-react-native';

import { useAssets } from 'expo-asset';

export default function App() {
  const [assets, error] = useAssets([require('./assets/slime.riv')]);
  const animationUrl = assets?.[0].localUri;

  return (
    <View style={styles.container}>
      {animationUrl && 
        <Rive url={animationUrl} fit={Fit.Contain} alignment={Alignment.Center} style={styles.animation} autoplay />
      }
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  animation: {
    width: 400,
    height: 400,
  }
});
telliott22 commented 2 years ago

This worked for me

fullsnack-DEV commented 2 years ago

getting Error "RiveReactNativeView" was not found in a UI Manager

tom4k commented 2 years ago

"RiveReactNativeView" was not found in a UI Manager

same here

fullsnack-DEV commented 2 years ago

Have you found any solution on it ?

deathemperor commented 2 years ago

I think managed expo doesn't support rive-react-native yet

suiyi8760 commented 2 years ago

Seems eject is needed to do pod install step for ios environment

thejustinwalsh commented 2 years ago

I thought I didn't do anything special but just checked out my demo project and I am using Expo SDK > 42 and used EAS to build a custom Expo Application for testing the native extensions. EAS did the work of giving me an Expo Go app with the native dependencies.

nandorojo commented 2 years ago

Just to clarify for everyone, you don't need to "eject". You just need a custom dev client, made with expo prebuild and expo run:ios rather than Expo Go. Or, you can build the dev client/final app on EAS. As long as you don't use Expo Go, then you can use any native library with Expo.

nandorojo commented 2 years ago

For what it's worth, expo prebuild didn't work for me, since I got a minimum deployment error. I fixed it by adding this plugin to my app.config.ts:

import { withPodfileProperties } from "@expo/config-plugins"

export default {
  plugins: [
    withPodfileProperties,
    (config) => {
      config.modResults = {
        ...config.modResults,
        "ios.deploymentTarget": "14.0",
      };
      return config;
    },
  ],
}

Then expo run:ios worked for me.

thehobbit85 commented 1 year ago

Looks like @nandorojo solution dosn't work for me. I keep getting:

✖ Config sync failed
TypeError: [ios.podfileProperties]: withIosPodfilePropertiesBaseMod: action is not a function
TypeError: [ios.podfileProperties]: withIosPodfilePropertiesBaseMod: action is not a function

But I did found a way around it which does work.

This is how my app.config.ts looks like:

const baseConfig = {
  name: 'app',
  slug: 'app',
  version: '1.0.0',
}

export default {
  ...baseConfig,
  mods: {
    ios: {
      podfileProperties: (config: { modResults: any }) => {
        config.modResults = {
          ...config.modResults,
          'ios.deploymentTarget': '14.0'
        }
        return config
      }
    }
  }
}

Then 'expo prebuild' and then expo run:ios and everything works for me.

nandorojo commented 1 year ago

good to know, this may have changed with sdk 47

thehobbit85 commented 1 year ago

I was just about to add that my solution is tested on expo version 47.0.8. So yeah, you're probably right. Also, rive-react-native version 3.0.41.

dmahajan980 commented 1 year ago

@nandorojo @thehobbit85 you can also set the minimum deployment version using the expo-build-properties config plugin.

{
  "plugins": [  
    [
      "expo-build-properties",
      {
        "ios": { "deploymentTarget": "14.0"  }
      }
    ]
  ]
}
nandorojo commented 1 year ago

@nandorojo @thehobbit85 you can also set the minimum deployment version using the expo-build-properties config plugin.


{

  "plugins": [  

    [

      "expo-build-properties",

      {

        "ios": { "deploymentTarget": "14.0"  }

      }

    ]

  ]

}

This is the recommended approach now.

dmahajan980 commented 1 year ago

Use Expo Asset to load animations

Using the expo-asset module we can retrieve a full file URI to the animation that is bundled into our app, and load the animations using the url prop on the Rive component.

App.tsx

import { StatusBar } from 'expo-status-bar';
import { Image, StyleSheet, Text, View } from 'react-native';
import Rive, { Alignment, Fit } from 'rive-react-native';

import { useAssets } from 'expo-asset';

export default function App() {
  const [assets, error] = useAssets([require('./assets/slime.riv')]);
  const animationUrl = assets?.[0].localUri;

  return (
    <View style={styles.container}>
      {animationUrl && 
        <Rive url={animationUrl} fit={Fit.Contain} alignment={Alignment.Center} style={styles.animation} autoplay />
      }
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  animation: {
    width: 400,
    height: 400,
  }
});

Unfortunately, this does not work on Android.

iway1 commented 1 year ago

Recommend the Rive team prioritizes seamless Expo managed workflow support. If it doesn't then it makes the lib unusable for like 50%+ of React Native projects

perroudsky commented 1 year ago

I'm able to make it work with expo custom dev client on ios, but don't you guys have this problem where the initial frame before playing the animation is not the actual first frame of the animation ?

dmahajan980 commented 1 year ago

I'm able to make it work with expo custom dev client, but don't you guys have this problem where the initial frame before playing the animation is not the actual first frame of the animation ?

Hi there @perroudsky! Can you share the original/sample repo showing how you did this on Android?

perroudsky commented 1 year ago

Hi @dmahajan980, sorry i was talking about ios. Haven't tried on android yet

sergioisidoro commented 1 year ago

If it doesn't then it makes the lib unusable for like 50%+ of React Native projects

Indeed, and maybe even more than 50% on newer projects. Nowadays Expo is the default way of using React native, so not supporting expo will really hamper rive's adoption on new projects.

nderscore commented 1 year ago

I came up with another strategy to integrate Rive animations into a React Native/Expo project. At the cost of a little bit of extra storage space and computation time, animations can be embedded into the JS bundle as base-64 encoded data-URI's.

Here's an example config using babel-plugin-inline-import-data-uri:

// babel.config.js
module.exports = (api) => {
  // ...
  return {
    plugins: [
      ['inline-import-data-uri', { extensions: ['.riv'] }]
    ]
  }
};
// App.tsx
import { View } from 'react-native';
import Rive from 'rive-react-native';

import MyAnimation from './my-animation.riv';

export default function App() {
  return (
    <View>
      <Rive
        autoplay
        url={MyAnimation}
        style={{ width: 400, height: 400 }}
      />
    </View>
  );
}

With this method, nothing needs to change in the metro configuration since it explicitly avoids using the RN/metro asset pipeline.


Sadly, just like the expo-asset method, this method only seems to work on iOS 😭 On Android, a native runtime error is logged:

[1047] NetworkDispatcher.processRequest: Unhandled exception java.lang.RuntimeException: Bad URL data:application/octet-stream;base64,...

The rive-react-native android implementation and the rive-android library both only support network protocols and not data:, because riveUrl is always processed with Android's Volley HTTP client, which is only meant to deploy network requests.

This might be the same issue that's causing the expo-asset method to fail on Android too, since it resolves to a file: protocol URL?


Update: I was able to work around this limitation on Android by patching the rive-react-native library to manually decode the data-URI:

diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
index 12a0d808a2abe4a8a1b887b1bc58614f4c73dd05..8d37080b94e2faa44e8613ab791fad4237891510 100644
--- a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
+++ b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
@@ -19,6 +19,7 @@ import com.facebook.react.bridge.ReadableArray
 import com.facebook.react.modules.core.ExceptionsManagerModule
 import com.facebook.react.uimanager.ThemedReactContext
 import java.io.UnsupportedEncodingException
+import java.util.Base64
 import kotlin.IllegalStateException

@@ -299,6 +300,24 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout

   private fun setUrlRiveResource(url: String, autoplay: Boolean = this.autoplay) {
+    if (url.startsWith("data:")) {
+      val b64data = url.substringAfter(',')
+      val decoder: Base64.Decoder = Base64.getDecoder()
+      val bytes = decoder.decode(b64data)
+
+      riveAnimationView.setRiveBytes(
+        bytes,
+        fit = this.fit,
+        alignment = this.alignment,
+        autoplay = autoplay,
+        stateMachineName = this.stateMachineName,
+        animationName = this.animationName,
+        artboardName = this.artboardName
+      )
+
+      return
+    }
+
     val queue = Volley.newRequestQueue(context)
     val stringRequest = RNRiveFileRequest(url, { bytes ->
       try {

Disclaimer: This patch is kind of quick and dirty - it could probably be cleaner and more error-safe.

tslater commented 1 year ago

@nderscore Has this Android patch been holding up? If so, Would it be worth creating a PR for it?

nderscore commented 1 year ago

@tslater I actually haven't been using it. The project I want to use Rive on doesn't have any animations (yet 😅)

I also don't love that my workaround relies on parsing somewhat-large data URL's in JS and passing them across the JS bridge to the native side - it's probably not very performant.

At some point, I would like to try an alternative patch to add local file URL support to the android portion of the library, which I think should unlock a path to using expo-assets instead. I just haven't prioritized investigating that path yet.

tslater commented 1 year ago

@dmahajan980 Do you know why this doesn't work on android? Is the native code just not set up for local file URLs?

enfipy commented 1 year ago

Has this issue been ignored by the Rive team so far? It looks like just because of it our team will stick with Lottie for a while.

It would be awesome to see Rive support for Expo out of the box.

thejustinwalsh commented 11 months ago

Completing the circular reference:

https://help.rive.app/runtimes/overview/react-native/loading-in-rive-files

I don't recall if resourceName was exposed or documented at the time of my initial request. Anyone on Android able to confirm that this method works?

expo-filesystem might be a better tool to use to get the paths to local assets here as well. I haven't tried this yet.

Lastly maybe having a ref method or prop that expects base64 encoded data is a solution that works as suggested above. expo-filesystem also exposes methods to return the riv file as a base64 encoded string through readAsStringAsync.

sugaith commented 10 months ago

@nderscore Fantastic! ty!

bryanltobing commented 6 months ago

as for me now. it's just make the app crash on ios and android, i already use prebuild

HayesGordon commented 5 months ago

Posting this here for visibility: Docs on how to get Rive working with Expo: https://rive.app/community/doc/adding-rive-to-expo/docFSwIlblYi

The suggestion for a normal react-native app on how to add Rive assets apply to expo as well: https://rive.app/community/doc/loading-in-rive-files/doc8P5oDeLlH

There is also a plugin that automatically brings in files. See this issue for more info: https://github.com/rive-app/rive-react-native/issues/185#issuecomment-1937039990

We can potentially add that to the package. Closing this issue as the above docs exist now, and we can track adding assets in the linked issue.

nderscore commented 5 months ago

@HayesGordon I'm not sure that I would consider this complete.

The documented solution requires a full rebuild of your native app (or your dev client) in order to add or update Rive assets. This also applies to the linked plugin.

Without explicit support for expo assets, it's missing two things that I would consider essential, as a developer building an app with Expo:

Without being able to use Expo Assets, this is just a vanilla react native integration. This is a blocker for me being able to adopt Rive in my app.

tslater commented 2 months ago

@HayesGordon I agree with @nderscore. There's a lot of developer pain involved here. Would you be open to a PR?

tslater commented 2 months ago

I finally have a solution that is in line with what I think we all are expecting. For now 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 get the url from 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
}

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

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

    return (
      <Rive
        ref={ref}
        {...riveProps}
        url={resolved.uri}
      />
    );
  }
);

Then you can call the component:

  <RiveAnimation
    source={require('assets/animations/your-animation.riv')}
   {...otherProps}
  />

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 without builds or prebuilds. 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.