How to build a React Native Android Library

Image for post
Image for post

As a Javascript developer, being asked to jump into the ‘Native’ part of React Native can be daunting. But I’m going to prove this wrong by fleshing out an easy way to create and use native code that can encapsulate a Java SDK in the form of a library. The docs for getting up and running with native code in React Native are super useful, alongside all the other articles available. However piecing this altogether into a working library can be challenging and time consuming.

For this reason I’ve created a boilerplate that gets you up and running with library development, this includes:

  • Executing a Promise in Java from a React Native app and changing state based on the result.
  • Executing a method in Java and listening to a callback for a response (DeviceEventEmitter). This mimics the behaviour you’ll most likely need when wrapping third party SDKs.
  • Testing with Jest.
  • A speedy development environment with a test application.
  • Best practices from react-native-create-bridge and react-native-create-library.

The best way to get up to speed with native development in a library is to try it out, so below we step through how to create the boilerplate. The code at the end of each step can be found in this repo under ‘STEP_NUMBER’

Step One - Javascript and Library Structure

Firstly it’s worth considering if you need a library. If you’re writing small amounts of native code tightly coupled with your application a Native Module built into your app could be the quickest and best solution. react-native-create-bridge is a great place to start; we’re going to be using code from here later on. If you’re unsure, start with this and take inspiration from the steps below as you face issues, it’s simple to move code from a module to a library.

We’re going to look at building a separate library that can be used across multiple apps and most likely used to expose large amounts of a native SDK. For example an SDK supplied by custom Android hardware.

Lets start off with the recommended tool from the React Native docs:

$ npm install -g react-native-create-library
$ react-native-create-library TestLib

This has given us a good base for the Library, however you’ll probably still feel a little confused about how this all fits together. To clear this up, the rest of this step is going to focus on the structure of our library’s code and creating an example app to test it out.

Example App

To start, we need to do some admin. We’ve created a folder called library and moved all the contents of the TestLib folder into it. Then running react-native init ExampleApp creates our test app.

Now lets define how we want to use our library by attempting to get a value from it, below are the changes to App.js:

  • import RNTest from 'react-native-test-lib’our library
  • state to hold the response from our native code
  • getNativeResult() which is an Async function, first setting state.nativeResult to ‘loading…’ and then to the response or error from our RNTest.getValaue() Promise
  • In our JSX we display this.state.nativeResult and add a button to call getNativeResult()

We also need to edit our package.json to include our library we just imported in App.js, alongside creating a way to update this library when we make changes. The following, updateLib Script, will remove node modules, clear the yarn cache, reinstall our packages and start up your React Native packager.

DEPENDENCY: "react-native-test-lib": "../library"SCRIPT: “updateLib”: “rm -rf node_modules/ && yarn --reset-cache && yarn start”

I’m sure you could improve this to work without removing all node_modules. It’s also worth noting you’ll need to rebuild and install your app if you’ve changed native code.

Library Scaffolding

Our goal is to create clearly defined layers of abstraction all the way from Native to Javascript. This structure enforces the opinion that writing Javascript is preferred over Java.

Following this rule we’ve created a testBridge folder to contain all of our Javascript code:

> testBridge
> __tests__ -> Will contain our tests
> bridgeOperations -> Operations available in native
getValue.js -> Calls the getValue native code
index.js -> Defines the bridge and exports operations
> library -> Contains the features of our library
index.js -> Crafts the library API
index.js -> exports our library to the world 🚀

Starting from the bottom, testBridge/bridgeOperations/index defines the bridge we are using, in this case NativeModules.RNTestLib and passes this bridge into all the bridgeOperations (e.g. getValue), these are the Java methods available and each are defined in their own file. There are two reasons for this: firstly we’re going to want the ability to mock the bridge for testing; secondly what if the native code exposed is complicated and the operation needs to be cleaned up. For example, say we execute a Java method which has a separate callback method and we use DeviceEventEmitter to emit the callback value. I’m sure our library code, more on this next, would appreciate receiving a promise and not having to deal with the inner workings of the native code and event emitter.

Moving on up, we are exporting an API crafted in testBridge/library/index, this folder may well contain other files that create the features of your library such as joining multiple bridgeOperations into one clean API. An example is that you want your library to export one connect function however you’d like it to conduct two bridgeOperations: checkPermission and connect this is the location of the files that will allow you to do this. Lets summarise with a badly drawn diagram:

Image for post
Image for post

What we’ve created here are clear levels of abstraction from the native code. /bridgeOperations needs to understand the inner working of our native code in order to export a clean Javascript interface while/library is a place to craft features from bridgeOperations to define the API our app will use.

This is a great time for you to have a play with the structure and get comfortable. You will be able to run the example app, press the button and await a response from our bridgeOperations/getValue.js Promise. Everything we’ve discussed above can be found on the STEP-ONE branch.

Step Two - Native Integration

Now it’s time to switch that fake Promise in getValue.js to resolve a string we set in native code. It’s recommended to use Android Studio for the following native changes, open up library/android.

Library Changes

Our example app is on the latest version of React Native so we need to update our library’s build.gradle. Simply match up the two build.gradles, consider here the app that will be using your library and the feature you plan to integrate. Kick off a Gradle sync to confirm you’re all set!

Next up, the native code found in java/[…]/RNTestLibModule.java, this is where you’ll spent most of your time integrating native features and SDKs. The react-native-create-library boilerplate file looks a little bear here, what we want is a nice starting point but less comprehensive than the example in the React Native docs. If you tested out react-native-create-bridge mentioned earlier you’ll know this has a nice starting point, so we’re using their module.java as a starting point. It’s also super handy that their code includes links to examples in the React Native docs, for this reason we’re only going to add to this file and not remove. We just need to update the class names to match our library.

✅ best practices.

Finally we create the getValue Promise in our module to replace the mocked one:

import com.facebook.react.bridge.Promise;@ReactMethod // Notates a method that should be exposed to React
public void getValue(final Promise promise) {
promise.resolve("A real native value");
}

Remember, our Javascript currently returns that mock Promise, so let’s switch that to return the native one:

const getValue = bridge => bridge.getValue();

We’re also going to need to update our native modules name where we define the bridge in bridgeOperations/index, switch it to NativeModules.RNTestLibModule to match the class name we just set.

Example App Changes

To start off, we need to link the library which will tell our app to compile and make available the native code we just wrote. For this, you can follow the auto generated readme found in /library.

Now to take her for a spin. Run yarn updateLib and reinstall the app on your device, press the button to get ‘A real native response’ displayed.

✅ Execute a promise in Java

✅ Have a speedy development environment with a test app

The code for this step can be found under the STEP-TWO branch. A good task to try it out would be to pass a value down to the native code and return it to React creating a full loop.

Step Three

Lastly, we are going to look into how to handle listening to events and our approach to testing the library.

Emitting Events

As soon as you start digging into Java and integrating third party SDKs you’ll soon find that you want your Javascript to execute a native method which has a callback that returns the value you’re after. Luckily, this is easily achievable with the DeviceEventEmitter. Below you can see the method requestDeviceID which triggers what could be a callback from an SDK, here we can emit the value we want.

import com.facebook.react.bridge.Arguments;@ReactMethod
public void requestDeviceId() {
// A method to request the device ID, below you could be calling an SDK implementation
// Remember to consider error handing here to void app crashes
deviceID("10001"); deviceID("10001");
}

private void deviceID(String id){
// This might be a method within a class that implements the SDK you're using
// We use Arguments.createMap to build an object to return
WritableMap idData = Arguments.createMap();
idData.putString("id", id);
emitDeviceEvent("device-id", idData);
}

Native done. Time for the Javascript. Create a new file called requestDeviceId.js within bridgeOperations. This file allows us to connect the initial requestDeviceId method call with the emitted ‘device-id’ event while wrapping it all up in a clean promise for our library to use.

const requestDeviceId = (bridge, eventEmitter) => {
bridge.requestDeviceId();
return new Promise((resolve, reject) => {
const listener = eventEmitter.addListener('device-id', (response) => {
resolve(response);
listener.remove();
});
// Could add a listener for errors here too
});
};

export default requestDeviceId;

Now we need to update the bridgeOperations index file. In here we follow the same structure as creating the bridge for the eventEmitter this allows our library to have one core eventEmitter and allows us to mock it for testing. Don’t forget to export the new operation we created:

import { NativeModules, NativeEventEmitter } from 'react-native';
import getValue from './getValue';
import requestDeviceId from './requestDeviceId';

const bridge = NativeModules.RNTestLibModule;
const eventEmitter = new NativeEventEmitter(bridge);

export default {
getValue: () => getValue(bridge),
requestDeviceId: () => requestDeviceId(bridge, eventEmitter),
};

Lets test this out in the example app, we’ve added the following function to request the device id:

getDeviceId = async () => {
this.setState({ deviceId: "loading..."})
try {
const { id } = await RNTest.requestDeviceId()
this.setState({ deviceId: id})
} catch(e) {
this.setState({ deviceId: e})
}
}

Alongside a state called deviceId, a button to call this function and a <Text> tag displaying the state. Pressing the button will display the deviceId in our native code. Hopefully now you’re starting to piece together the flow.

✅ Listening to callbacks from Java.

Creating a feature

Right, time to show off the idea behind the library folder by joining the two native methods we’ve built into one clean feature that the library API will expose.

We’ve created a new folder in library called coolFeature.js. This will be where we join the two operations together. The code looks like this:

const coolFeature = async (bridgeOperations) => {
try {
const value = await bridgeOperations.getValue();
// Our emitter sends an object by creating a writable map
// Below we just destructure that object
const { id } = await bridgeOperations.requestDeviceId();
return `Device: ${id}, says you are seeing ${value} `;
} catch (e) {
throw (new Error(e));
}
};

export default coolFeature;

We also need to export our coolFeature as part of our library in library/index.js. To do this, simply add coolFeature: () => coolFeature(bridgeOperations) to the exported object.

Now copy the steps we just followed to create the deviceId UI for the cool feature UI.

Once you press the button, you’ll see our library is returning both of the previously independent operations as one cleanly joined up feature. Hopefully this demonstrates the power of this structure and how you can go about abstracting the native code’s inner working. Everything in /library is easily accessible by all javascript developers, /bridgeOperates are easy to understand however the inner working can be left to a developer who’s comfortable with the native code and SDKs.

Tests

Last but not least, tests! To get Jest up and running, add a .babelrc file at the top level of /library with the following content. We need this to tell Babel to transpile our Jest code.

{
"presets": ["env"]
}

To start, create a __test__ folder containing /library/coolFeature.js. We’re going to create a test to mock the responses of our native code to ensure coolFeature is returning a correctly structured string to the library user. Add the following code and run the test:

In the within the following folders __tests__/library :import coolFeature from '../../library/coolFeature';

const responses = {
value: 'A real native value',
requestId: { id: '10001' },
};

const bridgeOperations = {
getValue: jest.fn().mockReturnValueOnce(responses.value),
requestDeviceId: jest.fn().mockReturnValueOnce(responses.requestId),
};

describe('Cool feature', () => {
test('is returning a correctly structured string', async () => {
try {
const response = await coolFeature(bridgeOperations);
expect(response).toBe(`Device: ${responses.requestId.id}, says you are seeing ${responses.value}`);
} catch (e) {
throw (e);
}
});
});

This is just an example of how you can go about testing the library features by mocking the bridgeOperations. You can apply the same principle necessary to our bridgeOperations by mocking the bridge and the eventEmitter.

✅ Library testing

If you want to simply add this library to another app, when hosted on a private Github repository, add it as a dependency like so:

"react-native-test-lib": "git+ssh://git@github.com/COMPANY/REPO.git#BRANCH",

And that’s a wrap! You should understand and have all the tools you’ll need to build a clean native library that can expose third party SDKs or do whatever you can imagine. The code for this step, can be found under the STEP-THREE branch.

Written by

Front-end Developer - React, React Native & Javascript. Bournemouth. @mitchclay0

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store