React Native - Guide to Integration with Existing Apps [Android]
I’m really enthusiastic about React Native and the way it composes with the native technologies. I’ve been interested in topics related to so-called brownfield integrations for quite a while now. I owe a lot to the amazing React Native community, and I would like to contribute back to it by expanding the available resources in this domain. In you’re interested in a more detailed explanation of my motivation, feel free to check out my iOS article. If not, let’s jump into the business at hand.
In this article, we’ll go through the key elements of brownfield integration for Android. We’ll integrate React Native dependencies directly into the native Android app, which is ok for learning purposes or small projects, but usually you'll want to follow the more independent approach described in the advanced guide.
Let’s begin by following the Android environment setup for RN CLI Quickstart from React Native documentation.
Integrating React Native Dependencies
Similarly to the approach taken in the iOS guide, for learning purposes we’ll opt out from the recommended approach to put all the files to the
/androidfolder. The native apps usually live in their own project directory, and changing the paths is fairly simple. Moreover, it encourages us to understand what’s really happening instead of simply copy-pasting the code. Please pay attention to these paths, as you may need to adjust them to point to the node_modules.
Adding React Native Dependencies
We’ll begin by adding the dependencies necessary to run React Native. Let’s create a new
package.jsonfile and choose the React Native version. Please keep in mind that a specific React version is bound to a given React Native version. You can look up React Native’s peer dependencies to find appropriate React version.
npm ito install React Native dependencies.
Fixing CLI Configuration
This step is usually omitted when the android project root is under the
/androidfolder, but because we want to keep it on the root level, we’ll need to configure the CLI using a
react-native.config.jsfile, which should look as follows:
Adding Gradle Dependencies
Now it's time to jump into the native side. We'll begin by adding repositories to our dependency management. Head to the
settings.gradlefile and add the following repositories to the dependencyResolutionManagement section.
Alternatively, you can use your project's
build.gradlefile and add them in the allprojects block.
NOTE: you don’t need to add JSC-android source if you use Hermes and want to avoid using JSC at all.
Adding React Native CLI Gradle Scripts
Once the dependencies are added, it’s time to add React Gradle scripts. To do this, simply open the
app/build.gradlefile and following lines:
First, we configure react scripts passing a hash map to the project.ext.react. This step might be optional if you use
/androidas a project root folder and if you want to opt-out from hermes. In our case we need a custom root and we want to use hermes as a JS engine, hence this configuration.
Adding React Native Dependencies
We continue working in the
app/build.gradlefile, as we need to include React Native dependencies in the project. We’ll start by defining a helper flag. This step is optional, but it is helpful if you’d like to be able to toggle between JSC and Hermes. If you’d like to use just one, feel free to omit this step.
Then, we head to the dependencies section and start adding dependencies required for React Native to work properly. The complete section should look like this:
Let’s consider all the dependencies that were added. The first and most important one is React Native:
It simply adds pre-bundled React Native files from
node_modules/react-native/android, and it’s the essential module for any React Native app.
Then we add swiperefreshlayout, as it’s used by React Native and isn’t included transitively.
Next, we need to add a JS engine, hence following lines:
You don’t need to create this condition if you want to choose just one JS engine. I’ve added it, as I want to be able to toggle between both easily.
Autolinking Native Modules
Starting from React Native 0.60, we can use autolinking to automatically link native dependencies to our app. This requires us to add some gradle scripts to our app. The first one should land in
These take care of adding appropriate dependencies to the dependencies section. It happens on the build time, so you don’t need to explicitly add them to your project.
The last step, required to fix our builds (and some runtime functionalities), is configuring the manifests. We’ll use two separate
AndroidManifest.xmlfiles - debug and release. In case you don’t have a debug manifest, create one in
app/src/debug/AndroidManifest.xml. The contents should look as follows:
Let’s break down what’s happening here. First, we add SYSTEM_ALERT_WINDOW to display error overlay in debug. Next, we set android:usesCleartextTraffic to allow connection with Metro bundler. Finally, we include DevSettingsActivity to display the React Native settings context menu.
Release manifest usually doesn’t have to change that much. At this point we just need to ensure that the package name is set and add the INTERNET permission.
At this point you should be able to sync the project with gradle files without any issues.
Create JS Entry Points
Let’s create a new
index.jsfile in the root folder and put a simple React component inside it.
Next, we register the component to the AppRegistry, which is an entry point for the React component tree. Please keep in mind that you can register multiple components to AppRegistry and attach them to the native UI tree using ReactRootView. When creating an instance of ReactRootView, the code within the component body will be executed.
Summing this up, if we add the following console.logs to our app:
The first one (A) will be executed when React Context is created, and the second one (B) will be run when we create the instance of ReactRootView that references MyReactComponent.
Creating a React Context
Creating React Native Host
On Android we’ll follow a similar, but slightly different approach than on iOS. We won't be instantiating a bridge per se, but rather creating React Native Host and React Context.
React Native Host is an abstract class that holds React Instance manager and exposes several configuration methods. You can use it to specify the main module name, bundle asset name, native packages, and other configurations. Like we did with iOS, we should do it once, store the instance, and re-use it across multiple instances of React Root View.
In order to store the instance of React Native Host (and in so doing the instance of React Instance Manager), we’ll follow a singleton pattern. Let’s create a BridgeManager file with loadReactNative function:
As you can see, within the body of the function we do three things:
- initialize SoLoader, which takes care of loading and unpacking native libraries. (In this context, native means NDK libraries using languages such as C or C++)
- create React Native Host and store its instance in the singleton
- create React Context in the background
Now let me elaborate on the last two things. As you can see, while creating React Native Host, we pass the basic configuration by overriding methods. First, we provide getJSMainModuleName - the path to the entry file loaded by metro bundle. In our case its
index.jsin the root folder, but you can also choose
index.tsor a different file. Then we override getBundleAssetName - the name of an asset with the release bundle. It defaults to index.android.bundle. Next, we override getPackages - a method where we provide the list of native modules (PackageList class is automatically generated by auto linking) (If you have any local native modules, this is the place to pass the packages). Finally, we override getUseDeveloperSupport, which toggles the ability to display the dev context menu.
There are several other configuration methods, like enabling lazy view managers, providing JSModulePackage, etc. I strongly recommend looking up the implementation of ReactNativeHost to see all options
Now in the main activity’s onCreate (or any other convenient place), simply call loadReactNative method. I usually do it on the app startup and hide the splash screen once it’s loaded.
Attaching a Root View
Once everything is set, it's time to render a Root View. Contrary to on iOS, except for creating an instance of the Root View, we will need to call a startReactApplication method. This method accepts three arguments, an instance of React Instance Manager, the name of the module registered to AppRegistry and Bundle with initial properties. Let’s create an instance of the root view and render a MyReactComponent within the main activity’s onCreate.
Fixing Dev Permissions for Error Overlay
Due to the fact that the Redbox must be displayed above other windows, we need to make sure that SYSTEM_ALERT_WINDOW is enabled in development builds. If the app targets API level 23 or higher, the app user must explicitly grant this permission to the app through a permission management screen. That’s why we need to add the following code to the MainActivity’s onCreate method.
Next, let’s override the onActivityResult method and handle when the permission is denied or granted.
Proxying lifecycle to React Instance Manager
In order for the app lifecycle to be fully aligned with React Native Instance Manager, we need to proxy the lifecycle methods from our Activity (or Fragment). Simply override the following lifecycles and invoke methods on the instance manager.
Handling Back Button
The next step would be to pass back button events to React Native. In order to do this, we need to override the onBackPressed method, implement DefaultHardwareBackBtnHandler, and proxy these events to React Instance Manager.
Hooking the Dev Menu
The last step is to connect the keyboard listener to the dev context menu. In the react native app, when you tap the menu button, the context menu appears. In order for this functionality to work we need to override the onKeyUp (or onKeyDown) method and show the options dialog when a certain key is pressed. In order to do this, we need to add the following code to our Activity:
If we'd like to use Flipper, we need to add Flipper dependencies that are used only in the debug builds. Of course this is an optional step, but I strongly recommend that you include it if you use hermes, as it strongly facilitates the debugging process.
Besides that, we need to create a ReactNativeFlipper file in the debug folder (
debug/com/package/ReactNativeFlipper.java), which should look as follows. You can use it later to configure flipper plugins.
Then in our MainActivity (or MainApplication if we have multiple activities), we need to create and call the initializeFlipper method:
And voila, you can now use the flipper to debug your brownfield application.
That’s all you need to do to integrate React Native 0.70+ to your Android application. Here’s the application:
If you’d like to see it in action, take a look at an example repo. As I mentioned at the beginning, this is a simple setup that introduces unnecessary complexity to the target codebase, so in real life you might consider the advanced setup described here.