Skip to content

software-mansion-labs/expo-brownfield-target

Repository files navigation

expo-brownfield-target by Software Mansion

Warning

As of Expo SDK 55 this library has become part of the Expo SDK under new expo-brownfield package and will not include any new features (or Expo SDK >= 55 support) in this repository. Please see: https://docs.expo.dev/versions/latest/sdk/brownfield/ for more details

expo-brownfield-target

expo-brownfield-target is a library which includes an Expo config plugin that automates brownfield setup in the project, CLI for building the brownfield artifacts and built-in APIs for communication and navigation between the apps.

📖 Documentation

Table of contents

Motivation

Brownfield approach enables integrating React Native apps into native Android and iOS projects, but setting it up, especially in Expo projects using Continuous Native Generation is a manual, repetitive, and pretty complex task.

This library aims to fully automate and simplify brownfield setup by including a config plugin that configures your project on every prebuild, built-in APIs for common use cases and CLI which builds the brownfield artifacts.

Such setup of brownfield allows for easy publishing to Maven, as XCFramework or using Swift Package Manager which enables e.g. simple and more independent cooperation of native and RN teams.

Features

  • Automatic extension of native projects with brownfield targets
  • Easy integration with Expo project via config plugin interface
  • Artifact publishing using XCFramework for iOS and Maven for Android
  • Configurability & customizability
  • APIs for bi-directional communication and navigation between the apps

Platform & Expo SDK compatibility

The plugin supports both Android and iOS. As of now we only support Expo SDK 54.

Usage

Installation

npm install expo-brownfield-target

Plugin setup

Add the config plugin to the "plugins" section in your app.json or app.config.js / app.config.ts:

{
  "expo": {
    "name": "my-awesome-expo-project",
    ...
    "plugins": [
      ... // Other plugins
      "expo-brownfield-target"
    ]
  }
}

If you want to pass any configuration options make sure to add the plugin as an array:

{
  "expo": {
    "name": "my-awesome-expo-project",
    ...
    "plugins": [
      ... // Other plugins
      [
        "expo-brownfield-target",
        {
          "android": {
            ...
          },
          "ios": {
            ...
          }
        }
      ]
    ]
  }
}

See configuration.md for full reference of configurable options.

Manual setup

All steps performed by the plugin can also be performed manually. Please refer to manual-setup.md for a full guide for manual setup.

Adding brownfield targets

The additional targets for brownfield will be added automatically every time you prebuild the native projects:

npx expo prebuild

Building with CLI

The plugin comes with a built-in CLI which can be used to build both Android and iOS targets:

npx expo-brownfield-target build-android -r MavenLocal

npx expo-brownfield-target build-ios

More details and full reference of the CLI commands and options can be found in cli.md.

Building manually

Brownfields can be also built manually using the xcodebuild and ./gradlew commands.

# Compile the framework
xcodebuild \
    -workspace "ios/myexpoapp.xcworkspace" \
    -scheme "MyBrownfield" \
    -derivedDataPath "ios/build" \
    -destination "generic/platform=iphoneos" \
    -destination "generic/platform=iphonesimulator" \
    -configuration "Release"

# Package it as an XCFramework
xcodebuild \
    -create-xcframework \
    -framework "ios/build/Build/Products/Release-iphoneos/MyBrownfield.framework" \
    -framework "ios/build/Build/Products/Release-iphonesimulator/MyBrownfield.framework" \
    -output "artifacts/MyBrownfield.xcframework"
./gradlew publishBrownfieldAllPublicationToMavenLocal

See publishing.md for more details about the publishing tasks.

Using built artifacts in native projects

Below snippets are taken from the examples of using brownfields inside native apps at: example/android, example/ios and example/ios-swiftui.

Android

// MainActivity.kt
package com.swmansion.example

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
import com.swmansion.brownfield.showReactNativeFragment
import com.swmansion.brownfield.BrownfieldActivity

class MainActivity : BrownfieldActivity(), DefaultHardwareBackBtnHandler {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        showReactNativeFragment()
    }

    override fun invokeDefaultOnBackPressed() {
        // ...
    }
}

Extending BrownfieldActivity enables automatic integration of onConfigurationChanged lifecycle event with Expo lifecycle dispatcher. You can also set it up manually using BrownfieldLifecycleDispatcher:

// MainActivity.kt
package com.swmansion.example

import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
import com.swmansion.brownfield.showReactNativeFragment
import com.swmansion.brownfield.BrownfieldLifecycleDispatcher

class MainActivity: AppCompatActivity(), {
  // ...

  override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    BrownfieldLifecycleDispatcher.onConfigurationChanged(this.application, newConfig)
  }
}

BrownfieldLifecycleDispatcher also includes onApplicationCreate method which accepts the application as it's only parameter, but this method shouldn't be called manually, as it's invoked in ReactNativeHostManager.

iOS (SwiftUI)

// MyApp.swift
import SwiftUI
import MyBrownfieldApp

@main
struct MyApp: App {
  @UIApplicationDelegateAdaptor var delegate: BrownfieldAppDelegate

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

BrownfieldAppDelegate integrates host app with ExpoAppDelegate and initializes the shared instance of ReactNativeHostManager. You can also initialize it manually:

// ContentView.swift
import SwiftUI
import MyBrownfieldApp

struct ContentView: View {
    init() {
        ReactNativeHostManager.shared.initialize()
    }

    var body: some View {
        VStack {
            ReactNativeView(moduleName: "main")
        }
    }
}

iOS (UIKit)

// AppDelegate.swift
import UIKit
import MyBrownfieldApp

@main
class AppDelegate: BrownfieldAppDelegate {
    var window: UIWindow?

    override func application(
      _ application: UIApplication,
      didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        super.application(application, didFinishLaunchingWithOptions: launchOptions)

        window = UIWindow(frame: UIScreen.main.bounds)
        let viewController = ReactNativeViewController(moduleName: "main")
        window?.rootViewController = viewController
        window?.makeKeyAndVisible()

        return true
    }
}

BrownfieldAppDelegate integrates host app with ExpoAppDelegate and initializes the shared instance of ReactNativeHostManager. You can also initialize it manually and control which of the app delegate methods you want to forward:

// AppDelegate.swift
import UIKit
import MyBrownfieldApp

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    override func application(
      _ application: UIApplication,
      didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        ReactNativeHostManager.shared.initialize()
        ReactNativeHostManager.shared.expoAppDelegateWraper?
            .application(application, didFinishLaunchingWithOptions: launchOptions)

        window = UIWindow(frame: UIScreen.main.bounds)
        let viewController = ReactNativeViewController(moduleName: "main")
        window?.rootViewController = viewController
        window?.makeKeyAndVisible()

        return true
    }
}

Using with Metro Bundler

Debug builds use bundle hosted by Metro server (hosted over localhost:8081) instead of the bundle included in the brownfield framework.

Be sure to start Metro server by running the following command in your Expo project:

npm start

Android

To be able to use Metro create a separate debug-only Manifest with the following contents in your native app which will host the brownfield:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <application
        android:usesCleartextTraffic="true"
        tools:ignore="GoogleAppIndexingWarning"
        tools:replace="android:usesCleartextTraffic"
        tools:targetApi="28" />
</manifest>

Then be sure to build and publish the artifacts using either All (includes both debug and release) or Debug configuration and to use the debug variant in the native app. Also, don't forget to reverse the port 8081 (if necessary):

adb reverse tcp:8081 tcp:8081

iOS

To use Metro server instead of bundle included at the build time, compile the brownfield framework using Debug configuration (-d/--debug flag when using the CLI). Debug XCFramework should automatically source the bundle from the Metro server.

Acknowledgments

Huge thanks to:

expo-brownfield-target is created by Software Mansion

swm

Since 2012 Software Mansion is a software agency with experience in building web and mobile apps. We are Core React Native Contributors and experts in dealing with all kinds of React Native issues. We can help you build your next dream product – Hire us.

Made by @software-mansion and community 💙

About

Expo config plugin extending native projects for building app as a brownfield

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors