React Native: Writing a native Android module

It’s difficult to find clear, up-to-date instructions for writing a native Android module for your React Native project.

I spent about half a day piecing this all together from a few sources, so here’s how it ended up.

Prerequisites:

  • React Native is installed and up and running
  • If you used expo, you should have ejected already, and already got the packager up and running again (it’s a little bit of a battle)
  • Android Studio is handy for pointing out syntax errors and code highlighting for .java files.

What will we achieve?

By the end of this article, you’ll have a module written in java that runs native code on your android device and returns the result to your javascript/React Native environment.

You’ll have:

  • A module, which holds the Java class that runs your native functionality.
  • A package, which connects (through code) your module to your React Native code.
  • A javascript connector that you can call easily from your React Native code.

Preparing the module file

Pick a name for your module. This article uses “MyTestModule” for a module name and com.tester.tester_nativemodules for a package name.

Using the above, create a subfolder for your project files under

<project root>/android/app/src/main/java/com/<projectname>/tester_nativemodules/

Create MyTestModule.java inside that folder, and paste the following code.

Note where I’ve bolded the package name, and the module name is italic.

package com.tester.tester_nativemodules;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.NativeModule;

import java.util.Map;
import java.util.HashMap;

public class MyTestModule extends ReactContextBaseJavaModule {
  public MyTestModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public String getName() {
    return "MyTestModule"; /* Must match class name above! */
  }

  // Available as NativeModules.MyTestModule.processString
  @ReactMethod
  public void processString(String personName, Callback callback) {
    callback.invoke("Hi there: " + personName);
  }
}

The function in blue is the actual code that will be available from ReactNative. You can make as many of these as you need.

@ReactMethod makes this function available over in React Native land.

You can use any primitive types you need in the method signature, and you can add as many of these methods as you need. Above, we accept a “personName” string and a callback. We’ll pass our function output to callback so it can make it’s way back to React Native land.

Creating the package

The package is a file that exists just to bridge our module with React Native. It’s pretty generic, just make sure to set your package name and module name correctly. I’ve bolded/italicised them again below.

This file is “MyTestPackage.java”, and lives alongside MyTestModule.java in <project root>/android/app/src/main/java/com/<projectname>/tester_nativemodules/

package com.tester.tester_nativemodules;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class MyTestPackage implements ReactPackage {

    @Override
    public List createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List createNativeModules(
            ReactApplicationContext reactContext) {
        List modules = new ArrayList<>();

        modules.add(new MyTestModule(reactContext));

        return modules;
    }

}

Link it to the Android Packages list

The last step on java side is telling the android project your package exists. Open up  <project root>/android/app/src/main/java/com/<projectname>/MainApplication.java and add your package name to the list of available packages. It should look something like this:

     protected List getPackages() {
       return Arrays.asList(
           new MainReactPackage(),
           new SvgPackage(),
           new MyTestPackage() /* The name of your package class above */
       );
     }

Using the package in javascript

The new package is available at NativeModules.MyTestModule.

Add an import at the top of your JS file,

import {NativeModules} from 'react-native';

and then you can call the processString function inside your JS file like this:

NativeModules.MyTestModule.processString("Rhys", function (response) {
  console.log(response); //Will log out "Hi there: Rhys"
})

All done

That’s all there is to adding native code to a React Native project. Adding something trivial like above ensures you have all the nuts and bolts in place before adding more complicated functionality.

I18n in Expo and React Native

Apparently, the whole world doesn’t speak english! Here’s how to add i18n support to your react native project. This article specifically addresses expo, since we’ll be working around i18n tools requiring react-native link.

We’re working on WooToApp, which is a native app builder for WooCommerce Stores. It now features internationalisation and language switching (of course!).

Add the i18n package.

The package is at https://github.com/AlexanderZaytsev/react-native-i18n if you’d like a read. There’s not much to it.

yarn add react-native-i18n or npm install react-native-i18n –save

In App.js we need two things done – load the translations, and load the user device locale.

Load the locale and the translations.

If you’re caching assets during app load (a common pattern for expo), add a promise to grab the device locale and store it in I18n.locale to your load routine.

While we’re here, we’ll define a few basic translations for our testing (down the track, you probably want to fetch() these from your server…). I’ve used Google Translate for this, which will do fine for the initial work of internationalisation.

const localeSet = Expo.Util.getCurrentLocaleAsync().then(function (r) {
  I18n.fallbacks = true;
  I18n.translations = {
    en: {
      pay: 'Pay eng',
      choosePaymentMethod: "Choose a payment method."
    },
    fr_FR: {
      pay: 'Payer',
      choosePaymentMethod: "Choisissez une méthode de paiement."
    },
  };
  I18n.locale = r;
  console.log(r);
  return r;
});

// load fonts, load business rules, load locale now.

Promise.all([...fonts, ...[business, localeSet]])

If you don’t have a preload routine like the above, you should make sure you’ve read and set the locale before outputting any strings.

Note that we’ve made use of a built in expo function to grab the users’ current locale. Expo.Util.getCurrentLocaleAsync() returns the users current locale, e.g. en_AU, fr_FR.
This function was previously Exponent.Util.getCurrentLocaleAsync().

Now we’ve worked out the users’ current locale, assigned it to the I18n library, and added some default translations. Note we’re also logging out the locale so you can check it out while debugging.

The next step is to translate a string in a component in your app – then you can continue internationalising the rest!

Translate strings in a component.

Open your component and import the I18n library.

import I18n from 'react-native-i18n';

Use a string translation you added above. Reload your app, and enjoy the translated goodness.

<Text>{I18n.t('choosePaymentMethod')}</Text>

Switching device locales (Android)

I used a french locale for my local device testing while writing this article.

To get to the locale switcher, I had to navigate:

Settings > General Management > Language & Input > Language. I added French (France) as a language and set that to my current.

It’ll help to keep that settings page open in your app drawer, or you’ll be navigating through a French menu to change it back.

React Native – using Flow

I’m backporting Flow into a large React Native project of mine. It’s unclear sometimes how to do things ‘the right way’ with Flow.

This is what’s working so far:

  • Any type that is used across multiple components should exist in it’s own JS file, and be imported as a type. Here’s how that looks in the component (e.g. follow the Order type from import. We expect to store an array of Order in this.state.orders.
  • Occasionally, rigorously defining a type just isn’t worth stuffing around with. For example, the navigation object passed around with react-navigation (via this.props.navigation. etc).
    I receive no real benefit from having this correctly typed. It’s not increasing visibility of my code or re-use, it’s not cleaning up bugs. After a quick google showed it’d be a battle, I settled that props.navigation is going to be of type any. *This is the only case of this. I might circle back and fix it later* 

    Here’s what that little cheat looks like:

    I should emphasise again, if you find yourself doing this often, you’re missing the point.

  • Passing functions in as props is a common pattern in React. Strongly typing these will quickly shrink your surface area for bugs (It’s amazing the bugs you find when backporting Flow into an existing project :D)

React Native: Testing with Jest

It’s a painful road getting an existing project set up with tests. An existing React Native app of ours, out in the wild, is getting retrofitted with unit tests.

The great part is, I know what to test. I know what type of bugs have been introduced in the past and what we’re likely to break again (exactly what the tests are for…)

“it renders without crashing”.

The out of the box test ensures your app renders without crashing. It’s also where you get to find out about any of your libraries that are incompatible with jest! For me, this was ‘sentry-expo’.

Simply updating to the most recent expo along the way had me on my way again.

 

import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';

it('renders without crashing', () => {
  const rendered = renderer.create().toJSON();
  expect(rendered).toBeTruthy();
  expect(rendered).toMatchSnapshot();
});

Note we’ve added an extra test (shown in bold) to take a DOM snapshot and check it against previous snapshots.

If the initial DOM tree changes in any way to the stored snapshot, the test will fail. This is a great way to capture unintended side-effects and loading issues. Once you’ve investigated the change (or pre-empted it..) you can tell the testrunner to update the snapshot for the future.

We keep “npm run test” alive in the background to see the tests constantly run as I make code changes, add new tests, and modify existing tests.

Testing a real component

Our retail price component accepts either a product or a price prop, and decides a price to show the user based on this. We also show the currency code decided by the app owner.

The component is in “components/Price.js”, so if we create a new file called “components/Price.test.js”, jest will find it and add it to the test suite automatically.

 <Price price='9.99'/>

or

 <Price product={{price: 101}}/>

Our first test creates a component with price of 9.99 and checks the state has been set correctly.

 

import React from 'react';
import renderer from 'react-test-renderer';

// the component in test
import Price from './Price';
// the global business settings (like currency symbol)
import Business from "../skin/business";


it('can-show-prices', () => {
 let pricetag = renderer.create(<Price price='9.99'/>);
 
// check the Text component (with class 'actualPrice') contains $9.99
 let pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["$", "9.99"]);
 
// check the snapshot matches, and make sure the price makes its way to state properly
 let pricetagTree = pricetag.toTree()
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.props.price).toBe('9.99');
 expect(pricetagTree.instance.state.price).toBe('9.99');

// Let's change the currency symbol for the app, and check it propagates to a component.
 Business.business.currency_symbol = "@";
 
// Pass in an object instead of a value.
 pricetag = renderer.create(<Price product={{price: 101}}/>);
 
// Make sure the new currency symbol and the price finds its way to the display (also, number formatting!)
 pricetagroot = pricetag.root;
 expect(pricetagroot.findByProps({className: "actualPrice"}).props.children).toEqual(["@", "101.00"]);
 
// Check the snapshot and that the price propagated correctly.
 pricetagTree = pricetag.toTree();
 expect(pricetagTree).toMatchSnapshot();
 expect(pricetagTree.instance.state.product.price).toBe(101);

});

Testing an external HTTP request

Testing an external HTTP request is strictly an integration test, not a unit test. It’s generally frowned upon to include ajax requests in your test suite, because you need to test your current code is working (the rest of the pipeline is a different problem, strictly).

However, an integration test that includes some end to end components holds a huge amount of value – knowing an API endpoint is still up and responding, authenticating properly, and that little used areas of your app are running as they should.

In our React Native context, calling an endpoint using our api.js file ensures that the current API adapter, as is in the codebase, can authenticate a user or initiate a payment.

To get ajax working correctly in jest, we need to require xhr2 first. Our underlying API adapter uses fetch(), which isn’t defined in jest.

const XMLHttpRequest = require('xhr2');

global.XMLHttpRequest = XMLHttpRequest;

Next up, we call the endpoint and investigate the response. This test calls the endpoint with invalid data, and ensures that the request fails (just as important as making sure a valid request passes!)

it("Should throw errors on incomplete request.", (done) => {
    return Woo.customers.authenticate("[email protected]").then( data => {
        expect(data).toEqual(expect.objectContaining({result:false,user:null,errors:expect.anything()}));
        expect(data.errors).toEqual(expect.objectContaining({invalid_username: expect.anything()}));
        done();
    }).catch(data => {
        console.log(data);
        done.fail();
    });
});

Woo.customers.authenticate is the API request exactly as we make it in the React Native app, using the same included JS file.

We interrogate the JSON response – the first expect ensures we receive an object with result set to false, user set to null, and an errors object (with anything in it).

The second expect ensures that the errors object has a property “invalid_username“.

We call ‘done()’ to let the test container know we’re finished (since we’re buried in a promise) – done() is a callback passed in to it().

More Jest & React Native testing snippets

It can be helpful to see some real world examples of tests – especially in jest I find the documentation a bit light on ‘real world’ tests. Here’s some Jest snippets from a React Native project:

Make sure string matches in JSON response.

expect(data.email).toEqual("[email protected]");

Make sure the Header component rendered with these props matches the last time it passed successfully.

let header = renderer.create(<Header title='test 234' showHamburger={true} isSearch={true} showCart={true} />);
expect(header).toMatchSnapshot();

Make sure the data object returned has a ‘liveMode’ property (which can be anything)

expect(data).toEqual(expect.objectContaining({liveMode: expect.anything()}));

Render a component. Then pass new props to component, and check the props rendered correctly as per last time:

let headerRenderer = renderer.create(<Header showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);
headerRenderer.update(<Header title={"RHYS"} showHamburger={true} isSearch={false} showBackButton={true} showCart={true}/>);


let headerJSON = headerRenderer.toJSON();
expect(headerJSON).toMatchSnapshot();

Good luck!

 

Fixing react native header bar (and status bar) on android and iOS.

The header bar in a new react native or expo project will either show as plain white in the background or eat up a good chunk of your app header area. This easy solution will pad out the header component by the height of the status bar, so it integrates nicely up top.

First up, import this helpful library

import {getStatusBarHeight} from 'react-native-status-bar-height';

Set up a stylesheet for the container and the header

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    header: {
        paddingTop: getStatusBarHeight(),
        height: 54 + getStatusBarHeight(),
    },
});

Now bind it to your Container and Header elements.

render() {

        return (<Container style={styles.container}>
            <Header hasTabs style={styles.header}>    
/* snip */

React Native: Example of full expo app.json

When I first started spending time with React Native, it was often a battle to work out just where and how things were supposed to be formatted in the app.json.

The base file doesn’t include the splash screen, app icon, notification icons, google signin nodes, and nodes required to build for the Play Store and the App Store.

Note the two bundle identifiers should always match (across android and ios). You should increment the 3 version identifiers in a uniform way unless you’re working on separate dedicated teams.

Here’s a lightly modified version of an app.json from a production app we’re working on. I’ll keep adding pieces to this script so it can become a kitchen sink file.

{
  "expo": {
    "sdkVersion": "26.0.0",
    "scheme": "foodorderingapprn",
    "name": "Food Ordering App",
    "version": "0.0.1",
    "orientation": "portrait",
    "slug": "food-ordering-app-rn",
    "icon": "./assets/logo-1024.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "cover",
      "backgroundColor": "#deff00"
    },
    "notification": {
      "icon": "./assets/notification-48.png",
      "color": "#deff00"
    },
    "android": {
      "config": {
        "googleSignIn": {
          "apiKey": "AIzaSyDCXXXXXXXPBTDw9-22222vvYQJiE11111",
          "certificateHash": "FFFFFFFFFFFFAA8BA9C47FF23A4E8FBAC5962675"
        },
        "branch": {
          "apiKey": "key_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        }
      },
      "package": "au.com.codeworkshop.food",
      "versionCode": 1,
      "permissions": []
    },
    "ios": {
      "config": {
        "branch": {
          "apiKey": "key_live_hdFKWmm3Yuv7LVjtujrx8knarvj1Yyla"
        }
      },
      "bundleIdentifier": "au.com.codeworkshop.food",
      "buildNumber": "1.0.1"
    },
    "extra": {
      "app_code": "ABCD"
    }
  }
}