-
Notifications
You must be signed in to change notification settings - Fork 146
RFC: standardize brownfield API for Android #953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hurali97
wants to merge
1
commit into
react-native-community:main
Choose a base branch
from
hurali97:proposal/android-brownfield
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+301
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,301 @@ | ||
| --- | ||
| title: Standardized Brownfield Integration APIs for React Native (Android) | ||
| author: | ||
| - Hur Ali | ||
| date: 4th Nov 2025 | ||
| --- | ||
|
|
||
| # RFC0000: Standardized Brownfield Integration APIs for React Native (Android) | ||
|
|
||
| ## Summary | ||
|
|
||
| This proposal introduces official abstractions and APIs in React Native Core to simplify brownfield (hybrid) integrations on Android. These APIs aim to standardize how existing native Android apps embed React Native screens and manage lifecycle, back press, and runtime initialization — all without requiring extensive manual setup or boilerplate code. | ||
|
|
||
| ## Basic example | ||
|
|
||
| ### Example: Embedding React Native in an existing Android app | ||
|
|
||
| ```kotlin | ||
| val defaultReactFragment = DefaultReactFragment(activity, launchOptions) { | ||
| override fun moduleName(): String = "RNProfileEntry" | ||
|
|
||
| override fun onHostResume() { | ||
| val defaultBackHandler = DefaultHardwareBackBtnHandler { | ||
| activity.onBackPressedDispatcher.onBackPressed() | ||
| } | ||
| return reactHost.onHostResume(activity, defaultBackHandler) | ||
| } | ||
| } | ||
|
|
||
| supportFragmentManager | ||
| .beginTransaction() | ||
| .add(R.id.container, defaultReactFragment) | ||
| .commit() | ||
| ``` | ||
|
|
||
| This demonstrates how native developers can present a React Native screen with minimal setup using the proposed DefaultReactFragment abstraction. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Brownfield adoption of React Native is a common real-world scenario where existing native apps incrementally embed React Native screens or features. While this is achievable today through Core classes, community plugins, and manual setup, there is no single, consistent API surface designed specifically for brownfield integration. | ||
|
|
||
| This creates friction for teams that want to: | ||
|
|
||
| - Embed React Native without restructuring their entire Android app. | ||
|
|
||
| - Avoid scattering React Native–specific interfaces and lifecycle handling throughout native codebases. | ||
|
|
||
| - Provide a smooth developer experience for both native and React Native engineers. | ||
|
|
||
| > This is often the first experience native developers have with React Native, it must feel straightforward and consistent. | ||
|
|
||
| By exposing a small set of official classes dedicated to brownfield usage, React Native Core can standardize integrations, reduce boilerplate, and improve long-term maintainability. | ||
|
|
||
| ## Detailed design | ||
|
|
||
| ### Current State of Brownfield Integration | ||
|
|
||
| There are two primary ways to embed React Native UI into an Android app: | ||
|
|
||
| 1. Configure React Native directly in the Android app | ||
|
|
||
| - Configure a Node environment within your Android project. | ||
|
|
||
| - Add react-android and hermes-android dependencies. | ||
|
|
||
| - Update build.gradle and settings.gradle. | ||
|
|
||
| - Extend ReactActivity and implement ReactApplication. | ||
|
|
||
| - Use these to launch React Native UI directly. | ||
|
|
||
| 2. Configure React Native in an Android library | ||
|
|
||
| - Create a library module inside a React Native app alongside the app module. | ||
|
|
||
| - Add react-android and hermes-android as api dependencies. | ||
|
|
||
| - Use community plugins (e.g., brownfield-gradle-plugin) to link native modules, assets, and JNI libs. | ||
|
|
||
| - Embed React Native UI from this library into the native app. | ||
|
|
||
| Both approaches are valid and come with trade-offs — the first offers direct integration, while the second encourages modularity and reuse. | ||
|
|
||
| <hr /> | ||
| <!-- ### Pain Points --> | ||
|
|
||
|
|
||
| ### 1. Initializing the React Native runtime | ||
|
|
||
| Developers can achieve this by subclassing their `MainApplication` with `ReactApplication`. | ||
|
|
||
| ```kotlin | ||
| class MainApplication : Application(), ReactApplication { | ||
|
|
||
| override val reactHost: ReactHost by lazy { | ||
| getDefaultReactHost( | ||
| context = applicationContext, | ||
| packageList = | ||
| PackageList(this).packages.apply { | ||
| // Packages that cannot be autolinked yet can be added manually here, for example: | ||
| // add(MyReactNativePackage()) | ||
| }, | ||
| jsBundleAssetPath = "index", | ||
| jsMainModulePath = "index.android.bundle", | ||
| useDevSupport = BuildConfig.DEBUG | ||
| ) | ||
| } | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
| loadReactNative(this) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| If for some reasons, the developers can not subclass their `MainApplication` with `ReactApplication`, they can manually create the `reactHost` and store a reference to it. | ||
|
|
||
| ```kotlin | ||
| val reactHost: ReactHost by lazy { | ||
| getDefaultReactHost( | ||
| context = application, | ||
| packageList = PackageList(this).packages, | ||
| jsBundleAssetPath = "index", | ||
| jsMainModulePath = "index.android.bundle", | ||
| useDevSupport = BuildConfig.DEBUG | ||
| ) | ||
| } | ||
|
|
||
| // invoke this function in MainApplication.onCreate | ||
| loadReactNative(this) | ||
| ``` | ||
|
|
||
| This API is in a good state and we do not propose any changes for this part. | ||
|
|
||
|
|
||
| ### 2. Presenting React Native UI | ||
|
|
||
| Current options: | ||
|
|
||
| - ReactDelegate – good for embedding smaller components, but limited lifecycle handling. | ||
|
|
||
| ```kotlin | ||
| val reactDelegate = ReactDelegate(activity, reactHost!!, componentName, initialParams) | ||
|
|
||
| reactDelegate.loadApp() | ||
| return reactDelegate.reactRootView as FrameLayout | ||
| ``` | ||
|
|
||
| - ReactFragment – wraps ReactDelegate, better for full-screen flows. | ||
|
|
||
| ```kotlin | ||
| val reactNativeFragment = ReactFragment.Builder() | ||
| .setComponentName("HelloWorld") | ||
| .setLaunchOptions(Bundle().apply { putString("message", "my value") }) | ||
| .build() | ||
|
|
||
| supportFragmentManager | ||
| .beginTransaction() | ||
| .add(R.id.react_native_fragment, reactNativeFragment) | ||
| .commit() | ||
| ``` | ||
|
|
||
| However, both require the hosting Activity to implement `DefaultHardwareBackBtnHandler`, introducing React Native–specific imports into native activities. This however, depends on the trade-off the native team is willing to take. If they are okay with colocating the RN internals in their Native Apps, this is not a problem for them. However, there are some teams who do not wish to take this path and want to abstract all of the RN internals in the AAR, this produces a problem for them. | ||
|
|
||
| > There is another way to present RN UIs by using ReactActivity. While this is a good option to have, not all teams would want to use it. | ||
|
|
||
| ### 3. Hardware Back Button Behavior | ||
|
|
||
| In mixed stacks, pressing back often skips the JS navigation stack entirely, e.g.: | ||
|
|
||
| ``` | ||
| Native App → MainActivity → NativeHomeFragment → ReactFragment("Settings") → JSscreenA → JSscreenB | ||
| ``` | ||
|
|
||
|
|
||
| Back press on JSscreenB can incorrectly return to NativeHomeFragment. Workarounds exist (e.g., intercepting OnBackPressedDispatcher), but Core support is needed for a clean developer experience. | ||
|
|
||
| <hr /> | ||
|
|
||
| ## Proposal: | ||
|
|
||
| Introduce official brownfield abstractions in React Native Core that mirror `DefaultReactHost` but for embedding UI. | ||
|
|
||
| ### 1. DefaultReactDelegate (new) | ||
| ```kotlin | ||
| val defaultReactDelegate = DefaultReactDelegate(activity, reactHost, launchOptions) { | ||
| override fun moduleName(): String = "RNProfileEntry" | ||
|
|
||
| override fun onHostResume() { | ||
| val defaultBackHandler = DefaultHardwareBackBtnHandler { | ||
| activity.onBackPressedDispatcher.onBackPressed() | ||
| } | ||
|
|
||
| return reactHost.onHostResume(activity, defaultBackHandler) | ||
| } | ||
| } | ||
|
|
||
| return defaultReactDelegate.reactRootView // returns the RN UI embedded | ||
| ``` | ||
|
|
||
| ### 2. DefaultReactFragment (new) | ||
| ```kotlin | ||
| val defaultReactFragment = DefaultReactFragment(activity, launchOptions) { | ||
| override fun moduleName(): String = "RNProfileEntry" | ||
|
|
||
| override fun onHostResume() { | ||
| val defaultBackHandler = DefaultHardwareBackBtnHandler { | ||
| activity.onBackPressedDispatcher.onBackPressed() | ||
| } | ||
|
|
||
| return reactHost.onHostResume(activity, defaultBackHandler) | ||
| } | ||
| } | ||
|
|
||
| supportFragmentManager | ||
| .beginTransaction() | ||
| .add(R.id.container, defaultReactFragment) | ||
| .commit() | ||
| ``` | ||
|
|
||
| These options will allow the developers to present their RN UI without requiring the host `Activity` to implement `DefaultHardwareBackBtnHandler`. This also lays the foundation to handle the JS stack back navigation behavior by allowing developers to specify their `defaultBackHandler`. | ||
|
|
||
| ### 2. Improved Back Press Integration (new) | ||
|
|
||
| With the above set, the developer can provide and handle the JS stack back navigation according to their needs and they can provide the defaultBackHandler, which will be executed on the last RN screen popped. | ||
|
|
||
| ```kotlin | ||
| val backPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { | ||
| override fun handleOnBackPressed() { | ||
| // invoked for JS stack back navigation | ||
| reactDelegate.onBackPressed() | ||
| } | ||
| } | ||
|
|
||
| activity.onBackPressedDispatcher.addCallback(backPressedCallback) | ||
|
|
||
| val defaultReactFragment = DefaultReactFragment(activity, launchOptions) { | ||
| override fun moduleName(): String = "RNProfileEntry" | ||
|
|
||
| override fun onHostResume() { | ||
| // invoked on the last RN screen | ||
| val defaultBackHandler = DefaultHardwareBackBtnHandler { | ||
| backPressedCallback.isEnabled = false | ||
| activity.onBackPressedDispatcher.onBackPressed() | ||
| } | ||
|
|
||
| return reactHost.onHostResume(activity, defaultBackHandler) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| To summarize, these APIs can be used by both options for brownfield integrations, one where devs directly integrate react-native with the native App and other where they opt for configuring RN as an android library. | ||
|
|
||
| - DefaultReactDelegate (new) | ||
| - DefaultReactFragment (new) | ||
| - Maybe we can rename the ReactFragment and confirm it to the proposal in this RFC | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| Why should we _not_ do this? Please consider: | ||
|
|
||
| - Increased API surface area in React Native Core. | ||
| - Potentially requires making `ReactDelegate.onHostResume` non-final. This is required to improve the back press integration. | ||
| - If introducing `DefaultReactFragment` we may need to drop `ReactFragment` as both serve the same purpose. We can either add the changes from this proposal directly to `ReactFragment` OR we can introduce `DefaultReactFragment` and mark `ReactFragment` as deprecated and remove in the future releases. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| - Continue using community-maintained plugins such as react-native-brownfield. | ||
| - Continue using the documentation or templates. | ||
| - https://www.rockjs.dev/docs/brownfield/android | ||
| - https://reactnative.dev/docs/integration-with-existing-apps | ||
|
|
||
| > However, these alternatives are dependent on the RN Core APIs and without brownfield dedicated APIs, they can produce a knock-on effect. One such example is the changes introduced from 0.80 onwards. Apart from it, some of these alternatives require more boilerplate from the native developer if they are opting for direct integration. | ||
|
|
||
|
|
||
| ## Adoption strategy | ||
|
|
||
| If we implement this proposal, how will existing React Native developers adopt it? Is this a breaking change? Can we write a codemod? Should we coordinate with other projects or libraries? | ||
|
|
||
| - Update the official docs to cover showing RN UI using `ReactActivity`, `DefaultReactFragment` and `DefaultReactDelegate`. | ||
| - https://reactnative.dev/docs/integration-with-existing-apps | ||
|
|
||
| ## How we teach this | ||
|
|
||
| - The docs can contain both the approaches, direct RN integration or library based RN inegration. We already have the direct RN integration docs. However, they can use some tweaks. For example, apart from showing how to initialize `reactHost` by subclassing `ReactApplication`, we can also show a way without a need to subclass `ReactApplication`. | ||
| - Update the official docs to cover showing RN UI using `ReactActivity`, `DefaultReactFragment` and `DefaultReactDelegate`. | ||
| - https://reactnative.dev/docs/integration-with-existing-apps | ||
|
|
||
| - Reference these and other APIs as the preferred approach for embedding RN into native Android apps. | ||
| - ReactActivity | ||
| - DefaultReactDelegate | ||
| - DefaultReactFragment(or ReactFragment) | ||
| - getDefaultReactHost | ||
|
|
||
| ## Unresolved questions | ||
|
|
||
| Should Core expose an API to preload JS bundles for faster first render? | ||
|
|
||
| > Historically, this was possible with `reactInstanceManager`.createReactContextInBackground(). Since `ReactNativeHost` is deprecated post–v0.82, there’s no clear alternative. Could reactHost.reload("reason") serve this purpose? | ||
|
|
||
| <hr /> | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this was supposed to be a section?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or maybe just remove it?