Skip to content

Installation ​

Published to npm as react-native-pose-detection. Both platforms are complete.

Requirements ​

React Native0.85+
Expo SDK56+ (dev client or EAS Build)
iOS16.4+, which Expo's ExpoModulesCore requires from SDK 56
AndroidAPI 24+
Architecturenew. React Native 0.82 removed the legacy one, so there is nothing to choose

Expo Go is not supported and never will be. This package contains native code.

Expo ​

bash
npm i react-native-pose-detection
json
{
  "expo": {
    "plugins": [
      ["react-native-pose-detection", {
        "model": "full",
        "cameraPermissionText": "We use the camera to analyze your movement."
      }]
    ]
  }
}
bash
npx expo prebuild
npx expo run:ios       # or run:android

Full plugin options: config plugin reference.

Bare React Native ​

bash
npm i react-native-pose-detection expo@56   # expo@57 on React Native 0.86
npx react-native-pose-detection fetch-model full
cd ios && pod install                       # after the wiring below

expo is not a typo and it does not turn your app into an Expo app. This package is built with the Expo Modules API, and that API's autolinking is what finds the native module. You need the expo package for autolinking. You do not need the config plugin, app.json, or prebuild.

Each Expo SDK is built against one React Native, so take the one that matches yours: expo@56 for React Native 0.85 and expo@57 for 0.86. npm cannot catch a mismatch, since expo accepts any React Native, and a mismatch shows up later: expo@57 on React Native 0.85 fails to compile for Android. So fetch-model warns about one and installs anyway, and doctor fails on it, both naming the version to install.

Starting a new app? Create it from Expo's bare template: a plain React Native app with android/ and ios/ checked in, on the React Native the newest Expo SDK is built for, with the wiring below and the iOS 16.4 target already done.

bash
npx create-expo-app@latest MyApp --template bare-minimum

The React Native CLI's default can have no Expo SDK at all, since Expo skips some React Native versions, such as 0.87. On one of those, doctor names the newest pair this package supports.

An app that already has expo keeps it. Install the package alone, npm i react-native-pose-detection: an expo@… in the command replaces the SDK you have, and npm does it without asking. Leave expo out, and npm adds the newest SDK, whatever your React Native is, which is why the command above names one. If the app uses other Expo packages too, npx expo install --check checks each of them against the SDK and names the versions to install; doctor only compares expo with React Native.

This package needs Expo SDK 56 and React Native 0.85 or newer. Below that npm refuses the install with ERESOLVE, naming peer expo@">=56"; yarn, pnpm and --legacy-peer-deps only warn and install it anyway, and doctor then fails on the version, see troubleshooting.

Wiring Expo modules into an existing app ​

On React Native 0.85, npx install-expo-modules@latest makes every edit below for you: it adds expo@56, raises iOS to 16.4 in both places and wires both native projects. Its version 0.16.0 knows Expo SDKs up to 56, so on React Native 0.86 it stops with Unable to find compatible Expo SDK version, and the edits are yours to make. A working copy of all of them is example/bare, which CI builds on every push that changes code.

android/settings.gradle, above include ':app', merged into the pluginManagement, plugins and extensions.configure blocks the template already has rather than added beside them:

groovy
pluginManagement {
  def expoPluginsPath = new File(
    providers.exec {
      workingDir(rootDir)
      commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
    }.standardOutput.asText.get().trim(),
    "../android/expo-gradle-plugin"
  ).absolutePath
  includeBuild(expoPluginsPath)
}

plugins { id("expo-autolinking-settings") }

extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
  ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
expoAutolinking.useExpoModules()
expoAutolinking.useExpoVersionCatalog()

android/build.gradle, next to the React Native line:

groovy
apply plugin: "expo-root-project"

MainApplication.kt, so Expo modules are registered with the host:

kotlin
import expo.modules.ExpoReactHostFactory

override val reactHost: ReactHost by lazy {
  ExpoReactHostFactory.getDefaultReactHost(applicationContext, PackageList(this).packages)
}

MainActivity.kt, so Expo modules see the activity callbacks:

kotlin
import expo.modules.ReactActivityDelegateWrapper

override fun createReactActivityDelegate(): ReactActivityDelegate =
    ReactActivityDelegateWrapper(
        this,
        BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
        DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled),
    )

iOS ​

ios/<App>/Info.plist. There is no manifest merging on iOS, so this key has to be yours:

xml
<key>NSCameraUsageDescription</key>
<string>We use the camera to analyze your movement.</string>

Deployment target 16.4 or higher, because that is what ExpoModulesCore requires from Expo SDK 56, and in two places, since React Native's template sets both to 15.1. In ios/Podfile, where a lower target makes Expo's autolinking silently skip every one of its pods, which surfaces as CocoaPods failing to find ExpoModulesCore:

ruby
platform :ios, '16.4'

And on the app target in Xcode, General → Minimum Deployments, which is IPHONEOS_DEPLOYMENT_TARGET in the project. Left at 15.1, the app stops compiling at compiling for iOS 15.1, but module 'Expo' has a minimum deployment target of iOS 16.4. doctor checks this one.

A bare app also needs Expo's autolinking in its Podfile, the counterpart of expo-autolinking-settings in settings.gradle. install-expo-modules writes it for you on React Native 0.85; on 0.86, add it by hand:

ruby
require File.join(
  File.dirname(`node --print "require.resolve('expo/package.json', { paths: [process.cwd()] })"`),
  "scripts/autolinking"
)

target 'YourApp' do
  use_expo_modules!
  # ...
end

And AppDelegate.swift, so the modules that autolinking found are registered when React Native starts, the counterpart of ExpoReactHostFactory on Android. With React Native's own factory the app builds and links, then fails at launch with Cannot find native module:

swift
internal import Expo
import React
import ReactAppDependencyProvider

@main
class AppDelegate: ExpoAppDelegate {
  var window: UIWindow?
  var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
  var reactNativeFactory: RCTReactNativeFactory?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    let delegate = ReactNativeDelegate()
    let factory = ExpoReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()
    reactNativeDelegate = delegate
    reactNativeFactory = factory

    window = UIWindow(frame: UIScreen.main.bounds)
    factory.startReactNative(withModuleName: "YourApp", in: window, launchOptions: launchOptions)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    bridge.bundleURL ?? bundleURL()
  }

  override func bundleURL() -> URL? {
#if DEBUG
    RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
    Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
  }
}

internal import matches how Expo's generated module provider imports it; a plain import Expo fails to compile against it. example/bare has the full file.

Android ​

android/build.gradle:

groovy
minSdkVersion = 24

The camera permission is already declared in this package's manifest and the merger adds it to your app. Declaring it again is fine, and worth doing so your own manifest tells the truth about what the app uses:

xml
<uses-permission android:name="android.permission.CAMERA" />

Granting it at runtime is still yours to do. The native view reports PERMISSION_DENIED and stops rather than prompting, because when to ask is a product decision.

Verify ​

bash
npx react-native-pose-detection doctor

EAS Build ​

Works with no extra configuration, prebuild runs in the build container and the plugin fetches the model there. Cache ~/.cache/react-native-pose-detection to skip the download between builds.

Android release builds ​

Ship an AAB. MediaPipe ships four ABI slices and a universal APK carries all of them: 10.1 MB for arm64-v8a, 7.1 MB for armeabi-v7a, 14.3 MB for x86 and 12.5 MB for x86_64, 44.0 MB of native library against the 10.1 MB a phone actually loads. Measured from an assembled APK on the pinned MediaPipe 0.10.35, see ADR 0007.

If you must ship an APK, filter it in your release build only:

groovy
android {
  defaultConfig {
    ndk { abiFilters "arm64-v8a" }
  }
}

Leave debug builds alone. Dropping x86_64 there is what breaks the standard Android Studio emulator on an Intel, Windows or Linux host.

MIT licensed. Built from the guides in the repository.