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

Android Release Builds Fail (RN 0.74+)

Open enchorb opened this issue 1 year ago • 3 comments

Description

iOS is fine and Android debug builds are fine. Android release builds fail. (React Native 0.74.1)

Get the error [TypeError: undefined is not a function]

Commenting out the Canvas and all elements within it makes it not crash so deff something related to Skia.

This was working fine on React Native 0.73.8 and Skia 0.1.229

Version

1.2.3

Steps to reproduce

Build release version of Android app

Snack, code example, screenshot, or link to a repository

import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, useMemo } from 'react'; import { StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View, ScrollView, Alert } from 'react-native'; import { Canvas, Group, LinearGradient, RoundedRect, vec, mix } from '@shopify/react-native-skia'; import { Easing, SharedValue, withTiming, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import * as Haptics from 'expo-haptics'; import { useSafeAreaInsets } from 'react-native-safe-area-context';

export interface ButtonGroupButton { label?: string; icon?: string; onPress?: () => void; multiPress?: boolean; }

export interface ButtonGroupRef { setButton: (idx: number | string, force?: boolean, action?: () => void) => void; next: () => void; previous: () => void; }

export interface ButtonGroupIdx { previous: number; next: number; current: number; }

export interface ButtonGroupProps { justification?: 'between' | 'even'; buttons: Record<string, ButtonGroupButton>; width?: number; height?: number; radius?: number; padding?: { x?: number; y?: number; }; disableHaptics?: boolean; duration?: number; state?: SharedValue<ButtonGroupIdx>; transition?: SharedValue; preButtonPress?: (idx: number, key?: string) => void; onButtonPress?: (idx: number, key?: string) => void; selectOnInit?: boolean; mask?: boolean; }

export const ButtonGroup = forwardRef<ButtonGroupRef, ButtonGroupProps>( (props: ButtonGroupProps, ref) => { const { buttons = {}, preButtonPress, onButtonPress, justification = 'even', disableHaptics, padding, duration = 750, selectOnInit = false, mask = false, // eslint-disable-next-line react-hooks/rules-of-hooks state = useSharedValue<ButtonGroupIdx>({ previous: 0, next: 0, current: 0 }), // eslint-disable-next-line react-hooks/rules-of-hooks transition = useSharedValue(0) } = props; const xPadding = padding?.x ?? 0; const yPadding = padding?.y ?? 0; const buttonKeys = Object.keys(buttons);

const { width: windowWidth } = useWindowDimensions();
const { top } = useSafeAreaInsets();

const width = useMemo(() => (props?.width ?? 64), [props.width]);

const height = useMemo(() => (props?.height ?? 64), [props.height]);

const radius = useMemo(() => (props?.radius ?? 16), [props.radius]);

const [scrollable, setScrollable] = useState<boolean>(false);
const [groupWidth, setGroupWidth] = useState<number>(windowWidth);
const [buttonPadding, setButtonPadding] = useState<number>(0);

const [buttonIdx, setButtonIdx] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);

const animateTimer = useRef<NodeJS.Timeout>(null);
const [animating, setAnimating] = useState<boolean>(false);

useImperativeHandle(ref, () => ({
  setButton: (idx: number | string, force = true, action?: () => void) =>
    onButtonSelect(idx, force, action),
  next: () => {
    onButtonSelect(state.value.next + 1);
  },
  previous: () => {
    onButtonSelect(state.value.next - 1);
  }
}));

useEffect(() => {
  const overflow = buttonKeys.length * width > groupWidth;
  setScrollable(overflow);
  setButtonPadding(
    overflow
      ? 0
      : (groupWidth - xPadding * 2 - buttonKeys.length * width) /
          buttonKeys.length /
          (justification === 'between' ? 1 + (buttonKeys.length - 2) / buttonKeys.length : 2)
  );
}, [buttons, groupWidth, justification]);

useEffect(() => {
  Alert.alert(`Canvas: ${!!Canvas}`, `Canvas`);
}, [Canvas]);

useEffect(() => {
  setAnimating(false);
  setLoading(false);
  onButtonSelect(0, selectOnInit);
}, []);

const onButtonSelect = async (idx: number | string, force = false, action?: () => void) => {
  let index;
  if (typeof idx === 'string') {
    const valIdx = buttonKeys.indexOf(idx);
    index = valIdx === -1 ? 0 : valIdx;
  } else index = idx;

  const btn = Object.values(buttons)[index];
  setButtonIdx(index);

  if ((state.value.next === index || btn?.multiPress) && !force)
    return !!btn?.multiPress ? btn?.onPress?.() : null;

  if (!disableHaptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

  setLoading(true);
  await preButtonPress?.(index, buttonKeys[index]);
  // @ts-ignore - Nullish Coalesce Void Functions
  Promise.resolve(action?.() ?? btn?.onPress?.())
    .then(() => {
      setAnimating(true);
      clearTimeout(animateTimer.current);
      animateTimer.current = setTimeout(
        () => {
          setAnimating(false);
        },
        force ? 0 : duration - 300
      );

      state.value = {
        previous: state.value.current,
        current: state.value.next,
        next: index
      };

      transition.value = 0;
      transition.value = withTiming(1, {
        duration,
        easing: Easing.inOut(Easing.cubic)
      });
    })
    .catch(() => {
      setButtonIdx(state.value.previous);
    })
    .finally(() => {
      setLoading(false);
      onButtonPress?.(index, buttonKeys[index]);
    });
};

const transform = useDerivedValue(() => {
  const { current, next } = state.value;
  const offset = width + buttonPadding * 2;
  return [
    {
      translateX: mix(transition.value, current * offset, next * offset)
    }
  ];
}, [state, transition, buttonPadding]);

return (
  <>
    {!!mask && (
      <View
        className={'bg-background absolute left-0 top-0 z-10 h-20'}
        style={{
          marginTop: -10 - top,
          width: windowWidth
        }}
      />
    )}

    <View
      style={{
        borderRadius: radius,
        pointerEvents: loading ? 'none' : 'auto'
      }}
      className={'bg-background-secondary z-10 w-full shadow-md shadow-black/25'}
      onLayout={({ nativeEvent }) => setGroupWidth(nativeEvent.layout.width)}
    >
      <View className={'w-full overflow-hidden'} style={{ borderRadius: radius }}>
        <ScrollView
          className={'w-full overflow-visible'}
          alwaysBounceVertical={false}
          horizontal={scrollable}
          showsHorizontalScrollIndicator={false}
        >
          <View
            className={'w-full flex-row'}
            style={{ paddingVertical: yPadding, paddingHorizontal: xPadding }}
          >
            <Canvas style={StyleSheet.absoluteFill}>
              <Group transform={transform}>
                <RoundedRect
                  x={justification === 'between' && !scrollable ? 0 : buttonPadding + xPadding}
                  y={yPadding}
                  height={height}
                  width={width}
                  r={radius}
                >
                  <LinearGradient
                    colors={['#31CBD1', '#61E0A1']}
                    start={vec(0, 0)}
                    end={vec(width, height)}
                  />
                </RoundedRect>
              </Group>
            </Canvas>
            {Object.entries(buttons).map(([value, btn], index) => (
              <TouchableWithoutFeedback
                key={value + index}
                onPress={() => onButtonSelect(index)}
              >
                <View
                  className={'items-center justify-center'}
                  style={{
                    width,
                    height,
                    borderRadius: radius,
                    marginRight:
                      justification === 'between' &&
                      !scrollable &&
                      index === buttonKeys.length - 1
                        ? 0
                        : buttonPadding,
                    marginLeft:
                      justification === 'between' && !scrollable && index === 0
                        ? 0
                        : buttonPadding
                  }}
                >
                  {loading && buttonIdx === index ? (
                    <></>
                  ) : (
                    <>
                    </>
                  )}
                </View>
              </TouchableWithoutFeedback>
            ))}
          </View>
        </ScrollView>
      </View>
    </View>
  </>
);

} );

enchorb avatar May 18 '24 00:05 enchorb

More info: Is working fine on Android versions 11 (API 30) and below

Additional stack:

2024-05-17 22:24:40.995 24513-24622 ReactNativeJS           pid-24513                            E  [TypeError: undefined is not a function]
2024-05-17 22:24:40.996 24513-24623 unknown:ReactNative     pid-24513                            E  TypeError: undefined is not a function, js engine: hermes, stack:
                                                                                                    clearContainer@1:2869768
                                                                                                    Eg@1:2835736
                                                                                                    Th@1:2850621
                                                                                                    Nh@1:2850245
                                                                                                    Ch@1:2847149
                                                                                                    Xc@1:2806156
                                                                                                    J@1:2867011
                                                                                                    R@1:2867353
                                                                                                    anonymous@1:284808
                                                                                                    _callTimer@1:283759
                                                                                                    _callReactNativeMicrotasksPass@1:283903
                                                                                                    callReactNativeMicrotasks@1:285898
                                                                                                    __callReactNativeMicrotasks@1:147100
                                                                                                    anonymous@1:146184
                                                                                                    __guard@1:146938
                                                                                                    flushedQueue@1:146095
                                                                                                    callFunctionReturnFlushedQueue@1:145951

enchorb avatar May 18 '24 01:05 enchorb

Upon even more testing and downgrading to React Native 0.73.8 (while keeping Skia at 1.2.3) the Android release builds work again. Seems to be some issue with 0.74+

enchorb avatar May 18 '24 12:05 enchorb

@wcandillon any updates here? seems to be an issue with the react-reconciler and Skia in React Native 0.74+ in Android release builds API 31 and above

enchorb avatar May 24 '24 11:05 enchorb

do you have a reproducible example? A project you could share on Github?

wcandillon avatar May 28 '24 13:05 wcandillon

Issue found, seems to be coming from react-native-skottie

enchorb avatar May 29 '24 00:05 enchorb

@enchorb did you file issue to RN-skottie? I faced this issue today and been pulling my hair as the stack only shows [TypeError: undefined is not a function], and no where pointing at skottie. Not until i found your issue, and replacing all my skottie back with rn-lottie and the issue's fix.

idrakimuhamad avatar Jun 19 '24 14:06 idrakimuhamad

I'm facing the same issue.🥲

Screenshot 2024-08-09 at 2 45 07 AM
"react-native": "0.74.5",
"@shopify/react-native-skia": "1.3.9",
"react-native-skottie": "2.1.4",
"react-native-reanimated": "3.12.1",

jeongshin avatar Aug 08 '24 17:08 jeongshin

I have the same issue, but I'm not convinced that Skottie is the issue, because in my case, Skottie components/animations work fine, it's when I render a Skia canvas in a completely different component that it crashes. Much like the original report, it only happens in release builds and only on Android.

The relevant section from adb logcat

10-21 23:06:36.276 4830 6858 W ReactNativeJS: Error: null 10-21 23:24:05.820 7583 7583 I ReactNative: [GESTURE HANDLER] Initialize gesture handler for root view com.facebook.react.ReactRootView{ccfa845 V.E...... ......ID 0,0-1440,2976 #b} 10-21 23:24:12.709 7583 7925 E ReactNativeJS: TypeError: undefined is not a function

I'm running:

"expo": "^51.0.38",
// ...
"react-native": "0.74.5",
// ...
"react-native-reanimated": "~3.15.5",
// ...
"react-native-skia": "^0.0.1",
"react-native-skottie": "^2.1.4",

The Skia canvas that I'm rendering uses Group (with clippingPath), Image, RoundedRect and LinearGradient.

Update: Nevermind, I removed Skottie and the error disappeared.

AdamGerthel avatar Oct 21 '24 21:10 AdamGerthel