Expo Integration

This guide walks you through packaging your Expo React Native app as an AAR or XCFramework and integrating it into your native Android or iOS application.

Prerequisites

  • Install the @callstack/react-native-brownfield package from the quick start section

Configuration

  1. Add the following block to your app.json file:
{
  "plugins": ["@callstack/react-native-brownfield"]
}
  1. Add the following scripts to your package.json, which will help us with generating the AAR and XCFramework. These are the scripts from brownfield-cli:
{
  "scripts": {
    "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release",
    "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib",
    "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release"
  }
}

The names brownfieldlib or BrownfieldLib are the default for AAR and Framework. You can configure different names, see here.

That is pretty much it from the configuration step. Now let's start with generating the AAR and later consuming it.

Android Integration

To integrate your Expo app into native Android app, you'll need to first package it as AAR file, publish it to local Maven registry (or remote repository) and consume from the Android app.

AAR Generation & Publication

  1. Let's first run the package command to build the AAR:
npm
yarn
pnpm
bun
deno
npm brownfield:package:android

This should only take a few minutes.

  1. Run the command to publish AAR to local maven
npm
yarn
pnpm
bun
deno
npm brownfield:publish:android

This should only take a few minutes.

That is all from the AAR steps. We can now consume the AAR inside a native Android App.

AAR: Present RN UI

  1. Call the ReactNativeHostManager.initialize in your Activity or Application:
ReactNativeHostManager.initialize(application) {
  Toast.makeText(
      this,
      "React Native has been loaded",
      Toast.LENGTH_LONG
  ).show()
}
  1. Add the following block to your Activity:
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    ReactNativeHostManager.onConfigurationChanged(application, newConfig)
}
  1. Use either of the following APIs to present the UI:
  • ReactNativeFragment
    • See the example here
  • ReactNativeFragment.createReactNativeFragment
    • Follow the docs here
  • ReactNativeBrownfield.createView

You can see the demo integration in Android App as well

Warning

Using the ComponentActivity does not work with Expo Apps as Expo needs an instance of AppCompatActivity. There is a hard type assertion here

  1. Build and install the android application 🚀

iOS Integration

To integrate your Expo app into native iOS app, you'll need to first package it as XCFramework file, and consume from the iOS app.

XCFramework Generation

  1. Let's first run the package command to build the XCFramework:
npm
yarn
pnpm
bun
deno
npm brownfield:package:ios

This should only take a few minutes.

XCFramework: Present RN UI

Pre-Requisites
  • Follow the step for adding the frameworks to your iOS App - here
  1. Call the following functions from your Application Entry point:
@main
struct IosApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    init() {
        ReactNativeBrownfield.shared.bundle = ReactNativeBundle
        ReactNativeBrownfield.shared.startReactNative {
            print("React Native has been loaded")
        }
        ReactNativeBrownfield.shared.ensureExpoModulesProvider()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
  1. Propagate the didFinishLaunchingWithOptions
class AppDelegate: NSObject, UIApplicationDelegate {
    var window: UIWindow?

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}
  1. Use the ReactNativeView or ReactNativeViewController to present the UI
ReactNativeView(moduleName: "ExpoRNApp")
  .background(Color(UIColor.systemBackground))

You can also use ReactNativeBrownfield.shared.view(moduleName, initialProps) which returns a UIView.

  1. Build and install the iOS application 🚀

Plugin Options

You can pass plugin options through the second item in the plugins tuple in app.json:

{
  "plugins": [
    [
      "@callstack/react-native-brownfield",
      {
        "ios": {
          "frameworkName": "MyBrownfieldLib",
          "bundleIdentifier": "com.example.app.brownfield",
          ...
        },
        "android": {
          "moduleName": "mybrownfieldlib",
          "packageName": "com.example.mybrownfieldlib",
          ...
        }
      }
    ]
  ]
}

iOS

  • frameworkName (string, default: "BrownfieldLib")
    • Name of the generated framework. This is also used as the XCFramework name.
  • bundleIdentifier (string, default: app bundle identifier + .brownfield)
    • Bundle identifier assigned to the generated framework.
  • buildSettings (Record<string, string | boolean | number>, optional)
    • Additional Xcode build settings applied when building the framework.
  • deploymentTarget (string, default: "15.0")
    • Minimum iOS version supported by the generated framework.
  • frameworkVersion (string, default: "1")
    • Framework version used for Apple build settings (must be an integer or floating point value, for example "1" or "2.1").

The plugin will determine the application target automatically. The auto-detection path works as follows:

  1. Common for all cases: scan the iOS targets for ones of type com.apple.product-type.application
  2. Use the first matching strategy:
    • CNG-derived name from mod compiler (modRequest.projectName) - only if it exists in the filtered application-type list of Xcode project targets
    • Unambiguous first application-type target - if there is exactly one
    • PBX "first native target" fallback - last resort fallback that selects the first native target of any type

Android

  • moduleName (string, default: "brownfieldlib")
    • Name of the generated Android library module.
  • packageName (string, default: app package name)
    • Package name used for the Android library module.
  • minSdkVersion (number, default: 24)
    • Minimum Android SDK supported by the library.
  • targetSdkVersion (number, default: 35)
    • Target Android SDK version for the library.
  • compileSdkVersion (number, default: 35)
    • Compile Android SDK version used to build the library.
  • groupId (string, default: package name)
    • Maven group ID used when publishing the AAR.
  • artifactId (string, default: module name)
    • Maven artifact ID used when publishing the AAR.
  • version (string, default: "0.0.1-SNAPSHOT")
    • Maven version used when publishing the AAR.

Need React or React Native expertise you can count on?