From e4dbbfa7f8527ac3e08d3690f3eb16cf512d05da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 12 Jan 2026 19:49:52 +0100 Subject: [PATCH 01/14] refactor(example): restructure app with demos, exercisers, reproducers sections Add category-based organization, order metadata for sorting, last opened persistence, and tests menu placeholder. --- .gitignore | 4 + example/package.json | 1 + example/src/App.tsx | 127 +++++++++- example/src/PagesList.ts | 83 ++++++- .../DataBindingArtboardsExample.tsx | 25 +- example/src/{pages => demos}/QuickStart.tsx | 3 +- .../{pages => exercisers}/ManyViewModels.tsx | 2 +- .../{pages => exercisers}/MenuListExample.tsx | 2 +- .../NestedViewModelExample.tsx | 2 +- .../{pages => exercisers}/OutOfBandAssets.tsx | 2 +- .../OutOfBandAssetsWithSuspense.tsx | 2 +- .../ResponsiveLayouts.tsx | 2 +- .../RiveDataBindingExample.tsx | 2 +- .../RiveEventsExample.tsx | 2 +- .../RiveFileLoadingExample.tsx | 4 +- .../RiveStateMachineInputsExample.tsx | 2 +- .../RiveTextRunExample.tsx | 2 +- .../ScriptingExample.tsx | 0 example/src/helpers/metadata.ts | 4 - .../src/pages/SharedValueListenerExample.tsx | 232 ------------------ .../Template.tsx} | 2 +- example/src/reproducers/local/.gitkeep | 0 .../src/{helpers => shared}/fileHelpers.ts | 0 example/src/shared/metadata.ts | 5 + example/src/{pages => tests}/TestsPage.tsx | 2 +- expo-example/app/index.tsx | 149 +++++++++-- expo-example/package.json | 1 + yarn.lock | 35 ++- 28 files changed, 383 insertions(+), 314 deletions(-) rename example/src/{pages => demos}/DataBindingArtboardsExample.tsx (91%) rename example/src/{pages => demos}/QuickStart.tsx (97%) rename example/src/{pages => exercisers}/ManyViewModels.tsx (99%) rename example/src/{pages => exercisers}/MenuListExample.tsx (99%) rename example/src/{pages => exercisers}/NestedViewModelExample.tsx (99%) rename example/src/{pages => exercisers}/OutOfBandAssets.tsx (98%) rename example/src/{pages => exercisers}/OutOfBandAssetsWithSuspense.tsx (99%) rename example/src/{pages => exercisers}/ResponsiveLayouts.tsx (98%) rename example/src/{pages => exercisers}/RiveDataBindingExample.tsx (98%) rename example/src/{pages => exercisers}/RiveEventsExample.tsx (98%) rename example/src/{pages => exercisers}/RiveFileLoadingExample.tsx (97%) rename example/src/{pages => exercisers}/RiveStateMachineInputsExample.tsx (97%) rename example/src/{pages => exercisers}/RiveTextRunExample.tsx (97%) rename example/src/{pages => exercisers}/ScriptingExample.tsx (100%) delete mode 100644 example/src/helpers/metadata.ts delete mode 100644 example/src/pages/SharedValueListenerExample.tsx rename example/src/{pages/TemplatePage.tsx => reproducers/Template.tsx} (93%) create mode 100644 example/src/reproducers/local/.gitkeep rename example/src/{helpers => shared}/fileHelpers.ts (100%) create mode 100644 example/src/shared/metadata.ts rename example/src/{pages => tests}/TestsPage.tsx (99%) diff --git a/.gitignore b/.gitignore index e22a908a..782cb7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ android/generated # ktlint binary cache .ktlint/ + +# Local reproducers (not checked in) +example/src/reproducers/local/* +!example/src/reproducers/local/.gitkeep diff --git a/example/package.json b/example/package.json index 39cbb3f9..7018208b 100644 --- a/example/package.json +++ b/example/package.json @@ -12,6 +12,7 @@ "test:harness:android": "react-native-harness --harnessRunner android" }, "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-picker/picker": "^2.11.4", "@react-navigation/native": "^7.1.9", "@react-navigation/stack": "^7.3.2", diff --git a/example/src/App.tsx b/example/src/App.tsx index 8291510c..5e282d45 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,13 +1,23 @@ +import { useEffect, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, + Alert, } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; -import { PagesList } from './PagesList'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { + PagesList, + PagesListByCategory, + type PageItem, + type Category, +} from './PagesList'; + +const LAST_OPENED_KEY = '@rive_example_last_opened'; type RootStackParamList = { Home: undefined; @@ -17,21 +27,92 @@ type RootStackParamList = { const Stack = createStackNavigator(); +function HeaderMenuButton() { + return ( + Alert.alert('Tests', 'TODO: Run tests')} + style={styles.headerButton} + > + + + ); +} + +const CATEGORY_LABELS: Record = { + demos: 'Demos', + exercisers: 'Exercisers', + tests: 'Tests', + reproducers: 'Reproducers', +}; + +function Section({ + title, + pages, + onNavigate, +}: { + title: string; + pages: PageItem[]; + onNavigate: (id: string) => void; +}) { + if (pages.length === 0) return null; + + return ( + + {title} + {pages.map(({ id, name }) => ( + onNavigate(id)} + > + {name} + + ))} + + ); +} + function HomeScreen({ navigation }: { navigation: any }) { + const [lastOpened, setLastOpened] = useState(null); + + useEffect(() => { + AsyncStorage.getItem(LAST_OPENED_KEY).then((id) => { + if (id) { + const page = PagesList.find((p) => p.id === id); + if (page) setLastOpened(page); + } + }); + }, []); + + const handleNavigate = (id: string) => { + AsyncStorage.setItem(LAST_OPENED_KEY, id); + navigation.navigate(id); + }; + return ( Rive React Native Examples - - {PagesList.map(({ id, name }) => ( + + {lastOpened && ( + + Recent navigation.navigate(id)} + onPress={() => handleNavigate(lastOpened.id)} > - {name} + {lastOpened.name} - ))} - + + )} + + {(Object.keys(CATEGORY_LABELS) as Category[]).map((category) => ( +
+ ))} ); } @@ -53,7 +134,10 @@ export default function App() { {PagesList.map(({ id, component, name }) => ( { + const module = context(key) as { default: PageType }; + const Component = module.default; + const id = key.replace(/^\.\//, '').replace(/\.tsx$/, ''); + + return { + id, + name: Component.metadata?.name ?? id, + description: Component.metadata?.description ?? '', + order: Component.metadata?.order ?? 999, + category, + component: Component, + }; + }) + .sort((a, b) => a.order - b.order); +} + +const demosContext = require.context('./demos', false, /\.tsx$/); +const exercisersContext = require.context('./exercisers', false, /\.tsx$/); +const testsContext = require.context('./tests', false, /\.tsx$/); +const reproducersContext = require.context('./reproducers', false, /\.tsx$/); + +// Try to load local reproducers (gitignored) +let localReproducersContext: __MetroModuleApi.RequireContext | null = null; +try { + localReproducersContext = require.context( + './reproducers/local', + false, + /\.tsx$/ + ); +} catch { + // local reproducers folder may be empty or not exist +} -export const PagesList: PageItem[] = pagesContext.keys().map((key) => { - const module = pagesContext(key) as { default: PageType }; - const Component = module.default; - const id = key.replace(/^\.\//, '').replace(/\.tsx$/, ''); +export const DemosList = loadPagesFromContext(demosContext, 'demos'); +export const ExercisersList = loadPagesFromContext( + exercisersContext, + 'exercisers' +); +export const ReproducersList = [ + ...loadPagesFromContext(reproducersContext, 'reproducers'), + ...(localReproducersContext + ? loadPagesFromContext(localReproducersContext, 'reproducers') + : []), +]; - return { - id, - name: Component.metadata?.name ?? id, - description: Component.metadata?.description ?? '', - component: Component, - }; -}); +export const TestsList = loadPagesFromContext(testsContext, 'tests'); + +export const PagesList: PageItem[] = [ + ...DemosList, + ...ExercisersList, + ...TestsList, + ...ReproducersList, +]; + +export const PagesListByCategory: Record = { + demos: DemosList, + exercisers: ExercisersList, + tests: TestsList, + reproducers: ReproducersList, +}; diff --git a/example/src/pages/DataBindingArtboardsExample.tsx b/example/src/demos/DataBindingArtboardsExample.tsx similarity index 91% rename from example/src/pages/DataBindingArtboardsExample.tsx rename to example/src/demos/DataBindingArtboardsExample.tsx index 8ed7a177..b1e6350b 100644 --- a/example/src/pages/DataBindingArtboardsExample.tsx +++ b/example/src/demos/DataBindingArtboardsExample.tsx @@ -13,22 +13,17 @@ import { type RiveFile, type BindableArtboard, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; -/** - * Data Binding Artboards Example - * - * Demonstrates swapping artboards at runtime using data binding. - * Based on: https://rive.app/docs/runtimes/data-binding#artboards - * - * The main Rive file includes a view model with a property of type `Artboard` - * called "CharacterArtboard". This property can be set to any artboard from - * either the main file or an external file. - * - * Rive source files: - * - Main: https://rive.app/marketplace/24641-46042-data-binding-artboards/ - * - Assets: https://rive.app/marketplace/24642-47536-data-binding-artboards/ - */ +/* + Data Binding Artboards + + Marketplace: + - Main: https://rive.app/marketplace/24641-46042-data-binding-artboards/ + - Assets: https://rive.app/marketplace/24642-47536-data-binding-artboards/ + + Docs: https://rive.app/docs/runtimes/data-binding#artboards +*/ export default function DataBindingArtboardsExample() { // Main scene file - contains the Card view model with CharacterArtboard property diff --git a/example/src/pages/QuickStart.tsx b/example/src/demos/QuickStart.tsx similarity index 97% rename from example/src/pages/QuickStart.tsx rename to example/src/demos/QuickStart.tsx index 3b37eb22..95c2493e 100644 --- a/example/src/pages/QuickStart.tsx +++ b/example/src/demos/QuickStart.tsx @@ -16,7 +16,7 @@ import { useViewModelInstance, Fit, } from '@rive-app/react-native'; -import type { Metadata } from '../helpers/metadata'; +import type { Metadata } from '../shared/metadata'; export default function QuickStart() { const { riveFile } = useRiveFile( @@ -73,6 +73,7 @@ export default function QuickStart() { QuickStart.metadata = { name: 'Quick Start', description: 'Basic data binding example with health and game over trigger', + order: 0, } satisfies Metadata; const styles = StyleSheet.create({ diff --git a/example/src/pages/ManyViewModels.tsx b/example/src/exercisers/ManyViewModels.tsx similarity index 99% rename from example/src/pages/ManyViewModels.tsx rename to example/src/exercisers/ManyViewModels.tsx index 5d2e32d6..0fd994f5 100644 --- a/example/src/pages/ManyViewModels.tsx +++ b/example/src/exercisers/ManyViewModels.tsx @@ -1,6 +1,6 @@ import { StyleSheet, View, Text, TouchableOpacity, Button } from 'react-native'; import { useState, useMemo, useRef, useEffect } from 'react'; -import type { Metadata } from '../helpers/metadata'; +import type { Metadata } from '../shared/metadata'; import { DataBindMode, RiveView, diff --git a/example/src/pages/MenuListExample.tsx b/example/src/exercisers/MenuListExample.tsx similarity index 99% rename from example/src/pages/MenuListExample.tsx rename to example/src/exercisers/MenuListExample.tsx index 6d5d4436..c6cf5dfc 100644 --- a/example/src/pages/MenuListExample.tsx +++ b/example/src/exercisers/MenuListExample.tsx @@ -17,7 +17,7 @@ import { useRiveList, useViewModelInstance, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function MenuListExample() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/NestedViewModelExample.tsx b/example/src/exercisers/NestedViewModelExample.tsx similarity index 99% rename from example/src/pages/NestedViewModelExample.tsx rename to example/src/exercisers/NestedViewModelExample.tsx index aae1264a..c90ef875 100644 --- a/example/src/pages/NestedViewModelExample.tsx +++ b/example/src/exercisers/NestedViewModelExample.tsx @@ -16,7 +16,7 @@ import { type RiveFile, type RiveViewRef, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function NestedViewModelExample() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/OutOfBandAssets.tsx b/example/src/exercisers/OutOfBandAssets.tsx similarity index 98% rename from example/src/pages/OutOfBandAssets.tsx rename to example/src/exercisers/OutOfBandAssets.tsx index 0fab9ae5..7c3fe7e3 100644 --- a/example/src/pages/OutOfBandAssets.tsx +++ b/example/src/exercisers/OutOfBandAssets.tsx @@ -12,7 +12,7 @@ import { RiveView, } from '@rive-app/react-native'; import { Picker } from '@react-native-picker/picker'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function OutOfBandAssetsExample() { const [uri, setUri] = React.useState('https://picsum.photos/id/372/500/500'); diff --git a/example/src/pages/OutOfBandAssetsWithSuspense.tsx b/example/src/exercisers/OutOfBandAssetsWithSuspense.tsx similarity index 99% rename from example/src/pages/OutOfBandAssetsWithSuspense.tsx rename to example/src/exercisers/OutOfBandAssetsWithSuspense.tsx index 2edba225..383c470e 100644 --- a/example/src/pages/OutOfBandAssetsWithSuspense.tsx +++ b/example/src/exercisers/OutOfBandAssetsWithSuspense.tsx @@ -15,7 +15,7 @@ import { type RiveImage, } from '@rive-app/react-native'; import { Picker } from '@react-native-picker/picker'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; class ErrorBoundary extends React.Component< { diff --git a/example/src/pages/ResponsiveLayouts.tsx b/example/src/exercisers/ResponsiveLayouts.tsx similarity index 98% rename from example/src/pages/ResponsiveLayouts.tsx rename to example/src/exercisers/ResponsiveLayouts.tsx index 0cdcde83..92132ad4 100644 --- a/example/src/pages/ResponsiveLayouts.tsx +++ b/example/src/exercisers/ResponsiveLayouts.tsx @@ -13,7 +13,7 @@ import { useRiveFile, type RiveViewRef, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * Demonstrates responsive layouts using Fit.Layout and layoutScaleFactor diff --git a/example/src/pages/RiveDataBindingExample.tsx b/example/src/exercisers/RiveDataBindingExample.tsx similarity index 98% rename from example/src/pages/RiveDataBindingExample.tsx rename to example/src/exercisers/RiveDataBindingExample.tsx index 2526ac00..d700eecb 100644 --- a/example/src/pages/RiveDataBindingExample.tsx +++ b/example/src/exercisers/RiveDataBindingExample.tsx @@ -11,7 +11,7 @@ import { useRiveTrigger, useRiveFile, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function WithRiveFile() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/RiveEventsExample.tsx b/example/src/exercisers/RiveEventsExample.tsx similarity index 98% rename from example/src/pages/RiveEventsExample.tsx rename to example/src/exercisers/RiveEventsExample.tsx index dc8fe5ed..413ef290 100644 --- a/example/src/pages/RiveEventsExample.tsx +++ b/example/src/exercisers/RiveEventsExample.tsx @@ -8,7 +8,7 @@ import { type RiveEvent, RiveEventType, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Rive events at runtime is deprecated. Use data binding instead. diff --git a/example/src/pages/RiveFileLoadingExample.tsx b/example/src/exercisers/RiveFileLoadingExample.tsx similarity index 97% rename from example/src/pages/RiveFileLoadingExample.tsx rename to example/src/exercisers/RiveFileLoadingExample.tsx index 5c8f91eb..6ea04ab5 100644 --- a/example/src/pages/RiveFileLoadingExample.tsx +++ b/example/src/exercisers/RiveFileLoadingExample.tsx @@ -13,8 +13,8 @@ import { type RiveFileInput, } from '@rive-app/react-native'; import { useState, useEffect } from 'react'; -import { downloadFileAsArrayBuffer } from '../helpers/fileHelpers'; -import { type Metadata } from '../helpers/metadata'; +import { downloadFileAsArrayBuffer } from '../shared/fileHelpers'; +import { type Metadata } from '../shared/metadata'; const LOADING_METHODS = { SOURCE: 'Source', diff --git a/example/src/pages/RiveStateMachineInputsExample.tsx b/example/src/exercisers/RiveStateMachineInputsExample.tsx similarity index 97% rename from example/src/pages/RiveStateMachineInputsExample.tsx rename to example/src/exercisers/RiveStateMachineInputsExample.tsx index a410234e..4a0ae8ee 100644 --- a/example/src/pages/RiveStateMachineInputsExample.tsx +++ b/example/src/exercisers/RiveStateMachineInputsExample.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { useEffect } from 'react'; import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Setting state machine inputs at runtime is deprecated. Use data binding instead. diff --git a/example/src/pages/RiveTextRunExample.tsx b/example/src/exercisers/RiveTextRunExample.tsx similarity index 97% rename from example/src/pages/RiveTextRunExample.tsx rename to example/src/exercisers/RiveTextRunExample.tsx index df514bb4..d0c98f8a 100644 --- a/example/src/pages/RiveTextRunExample.tsx +++ b/example/src/exercisers/RiveTextRunExample.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { useEffect } from 'react'; import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Setting text run values is deprecated. Use data binding instead. diff --git a/example/src/pages/ScriptingExample.tsx b/example/src/exercisers/ScriptingExample.tsx similarity index 100% rename from example/src/pages/ScriptingExample.tsx rename to example/src/exercisers/ScriptingExample.tsx diff --git a/example/src/helpers/metadata.ts b/example/src/helpers/metadata.ts deleted file mode 100644 index c10991cc..00000000 --- a/example/src/helpers/metadata.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Metadata = { - name: string; - description: string; -}; diff --git a/example/src/pages/SharedValueListenerExample.tsx b/example/src/pages/SharedValueListenerExample.tsx deleted file mode 100644 index 3a3e4f0e..00000000 --- a/example/src/pages/SharedValueListenerExample.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { - View, - Text, - StyleSheet, - Button, - ActivityIndicator, -} from 'react-native'; -import { type Metadata } from '../helpers/metadata'; -import Animated, { - useSharedValue, - useAnimatedReaction, - useAnimatedStyle, - withSpring, -} from 'react-native-reanimated'; -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from 'react-native-gesture-handler'; -import { useCallback, useEffect, useMemo } from 'react'; -import { - NitroModules, - type BoxedHybridObject, -} from 'react-native-nitro-modules'; -import { - Fit, - RiveView, - useRiveFile, - type RiveFile, - type RiveViewRef, - type ViewModelInstance, -} from '@rive-app/react-native'; - -export default function SharedValueListenerExample() { - const { riveFile, isLoading, error } = useRiveFile( - require('../../assets/rive/movecircle.riv') - ); - - return ( - - {isLoading ? ( - - ) : riveFile ? ( - - ) : ( - {error || 'Unexpected error'} - )} - - ); -} - -function WithViewModelSetup({ file }: { file: RiveFile }) { - const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]); - const instance = useMemo( - () => viewModel?.createDefaultInstance(), - [viewModel] - ); - - if (!instance || !viewModel) { - return ( - - {!viewModel - ? 'No view model found' - : 'Failed to create view model instance'} - - ); - } - - return ; -} - -function AnimatedRiveExample({ - instance, - file, -}: { - instance: ViewModelInstance; - file: RiveFile; -}) { - const progress = useSharedValue(0); - const startY = useSharedValue(0); - const viewRef = useSharedValue | null>(null); - - const boxedProperty = useMemo(() => { - const posYProperty = instance.numberProperty('posY'); - if (!posYProperty) { - return null; - } - return NitroModules.box(posYProperty); - }, [instance]); - - useAnimatedReaction( - () => progress.value, - (value: number) => { - 'worklet'; - if (!boxedProperty) return; - const property = boxedProperty.unbox(); - property.value = value; - - viewRef.value?.unbox()?.playIfNeeded(); - }, - [boxedProperty] - ); - - const panGesture = Gesture.Pan() - .onStart(() => { - 'worklet'; - startY.value = progress.value; - }) - .onUpdate((event) => { - 'worklet'; - progress.value = startY.value + event.translationY * 3; - }) - .onEnd((event) => { - 'worklet'; - // Use velocity from gesture to set initial velocity of spring - progress.value = withSpring(progress.value > 400 ? 800 : 0, { - damping: 10, - stiffness: 100, - velocity: event.velocityY * 3, - }); - }); - - const circleStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: progress.value / 3 }], - })); - - const animateTo800 = useCallback(() => { - progress.value = withSpring(800, { - damping: 8, - stiffness: 80, - }); - }, [progress]); - - const animateTo0 = () => { - progress.value = withSpring(0, { - damping: 8, - stiffness: 80, - }); - }; - - useEffect(() => { - animateTo800(); - }, [animateTo800]); - - return ( - - - Drag the blue circle to control position. Release to spring with - velocity. (Red = Rive, Blue = React Native) - - - - { - viewRef.value = NitroModules.box(ref); - }, - }} - /> - - - - - - -