gears

One advantage you often hear about with writing React Native applications is the ability to drop down into the native code whenever necessary.

At first, the thought of having to write native modules intimidated me, but as it turns out, it’s pretty simple. This post aims to de-mystify writing native modules and show how easy it can be with React Native.

Can you hear me now?

On a recent client project at PromptWorks, we needed to be able to prevent the user from performing an action that would play a sound if their volume was not turned up high enough. In order to do that, we needed to grab the device’s volume, but no such method existed in React Native or in any 3rd party packages (at the time).

Thankfully, getting the system volume is not difficult in iOS or Android, so it makes a perfect example of a simple native module. This post will walk through making a native module for iOS, for Android, and the JavaScript code we can use in our app to call our new modules. We’ll create a module with one method, getSystemVolume, that returns the system’s volume on a scale of 0 to 1.

If you’d like to jump straight to the example code, it’s all available on Github.

Note: We’re using version 0.40.0 of React Native for this example. Due to the fast moving nature of React Native, your mileage may vary if React Native has moved on!

Creating an iOS module with React Native

We’ll begin by adding the functionality to our iOS app.

We’ll need to add two files to our project: a header file and its implementation file. In Objective-C, the header file (with a .h extension) will declare the public interface of our class, while the implementation file (with a .m extension) will contain the actual implementation of our class. In our React Native module, we have a bit of a special case: we need only define the module in our header file as each method we want to expose is done so via the RCT_EXPORT_METHOD macro in the implementation file only.

Since we’re dealing with volume capabilities, we’ll use the imaginative names ios/<ProjectName>/Volume.h and ios/<ProjectName>/Volume.m (see this commit in our example code).

First up, the header file, Volume.h:

1
2
3
4
#import "RCTBridgeModule.h"

@interface Volume : NSObject <RCTBridgeModule>
@end

And its implementation file, Volume.m:

1
2
3
4
5
6
7
#import "Volume.h"

@implementation Volume

RCT_EXPORT_MODULE();

@end

NOTE: Since we didn’t pass any arguments to RCT_EXPORT_MODULE, the native module will take the name of our class, Volume.

You can create these files with your editor of choice, but if you’re using something other than XCode, be sure to add the files to the project in XCode (File > Add Files to "<ProjectName>"...). This will make some changes to project.pbxproj to include our new module.

Now, we have a bare-bones native module – it doesn’t do anything interesting yet, but it’s good to pause at this point. Unlike adding a JS module, the iOS app needs to be re-built before our native module is available (you’ll also need to re-build after each change). Now’s a good time to do that to ensure your app configuration is up-to-snuff.

NOTE: If you get an error like fatal error: 'RCTBridgeModule.h' file not found, ensure the “Header Search Paths” in the Build Settings tab of the Target configuration includes “$(SRCROOT)/../node_modules/react-native/React” with “recursive” set:

Example configuration
Make sure the Header Search Paths are set correctly.

Getting the system volume on iOS

We can now add a method to actually get the system volume in Volume.m (see this commit in our example code):

1
2
3
4
RCT_EXPORT_METHOD(getSystemVolume:(RCTResponseSenderBlock)callback) {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  callback(@[[NSNull null], @([session outputVolume])]);
}

Let’s unpack this a bit:

  • RCT_EXPORT_METHOD - the RCT_EXPORT_METHOD macro exports a method that will be available to our JavaScript code.
  • getSystemVolume will be the name of the available method.
  • (RCTResponseSenderBlock)callback specifies that getSystemVolume will take a single parameter that will be a callback.
  • AVAudioSession *session = [AVAudioSession sharedInstance]; and @([session outputVolume]) is the bit of code that actually gets the device’s volume.
  • callback(@[[NSNull null], @([session outputVolume])]); invokes the callback parameter with null passed in as the error (representing an error did not occur) and the volume as the second parameter.

Due to the asynchronous nature of the JavaScript code, we aren’t able to return a value from our method immediately. Instead, if we want our method to return the volume, we’ll need to pass in a callback that accepts the volume value (and a possible error).

Calling our new native method

Now, we could invoke our new method in our JavaScript code (see this commit in our example code):

1
2
3
4
import { NativeModules } from 'react-native'

const Volume = NativeModules.Volume;
Volume.getSystemVolume((error, volume) => window.alert(volume))

If we run this code in our app on the iPhone simulator, we’ll see an alert pop up with the simulator’s volume. Huzzah! But, if we try on Android, the app will crash and burn.

Creating an Android module with React Native

Next, let’s take on the Android module. We begin again by adding an empty module that will eventually house our new method. We can create a file android/app/src/main/java/com/reactnativebridgeexample/VolumeModule.java (see this commit in our example code):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.reactnativebridgeexample.volume;

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

public class VolumeModule extends ReactContextBaseJavaModule {
    private static final String TAG = "Volume";

    public VolumeModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return TAG;
    }
}

Most of this is boilerplate, but one important method is getName. The string returned by getName is how we will access our module in JavaScript, so this should match the name of the class in our iOS module – “Volume” in this case.

In order for our module to be included in the application, we need to wrap it up in a package and then add it to our ReactApplication. We’ll add a file at android/app/src/main/java/com/reactnativebridgeexample/VolumePackage.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.reactnativebridgeexample.volume;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
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 VolumePackage implements ReactPackage {

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

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

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

        modules.add(new VolumeModule(reactContext));

        return modules;
    }
}

This is mainly some necessary boilerplate that we’ll likely not need to look at again, so analysing this code is left as an exercise to the reader. The last step is adding our new package to our application. In our MainApplication.java file (android/app/src/main/java/com/reactnativebridgeexample/MainApplication.java), import our new package:

1
import com.reactnativebridgeexample.volume.VolumePackage;

and add it to the list of packages in getPackages:

1
2
3
4
5
6
7
@Override
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
      new VolumePackage()
  );
}

Now, when we next compile our Android application, the native module will be available.

Getting the system volume on Android

Just as we did with the iOS module, we can add a method to our module that will get the system’s volume (see this commit in our example code).

We’ll need to use the AudioManager to get the system volume, so add a couple imports to VolumeModule.java:

1
2
import android.content.Context;
import android.media.AudioManager;

And add an instance variable set in the constructor:

1
2
3
4
5
6
private AudioManager audio;

public VolumeModule(ReactApplicationContext reactContext) {
    super(reactContext);
    audio = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE);
}

Adding the actual method looks similar to the iOS version:

1
2
3
4
5
6
@ReactMethod
public void getSystemVolume(Callback callback) {
    int currentVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC);
    int maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    callback.invoke(null, ((float) currentVolume / maxVolume));
}
  • The ReactMethod annotation will make our method available in JavaScript.
  • The name of our exposed method matches the name of the method defined here, getSystemVolume.
  • The method again takes a single Callback argument.
  • After getting the volume, we can invoke the callback with any error as the first parameter and the system volume as the second parameter.

A new JS module for our new native module

One final improvement we can do is refactor our use of NativeModules into its own JavaScript module, volume.js (see this commit in our example code):

1
2
3
4
5
import { NativeModules } from 'react-native'

export const getSystemVolume = (callback) => {
  NativeModules.Volume.getSystemVolume(callback)
}

Then, we can get the volume using:

1
2
import { getSystemVolume } from './volume'
getSystemVolume((error, volume) => window.alert(volume))

A benefit of this refactoring is that if we want to perform different native code depending on the platform, we can isolate those code branches to their own (testable!) module instead of sprinkling Platform checks throughout our application code.

And that’s it! If you’d like to check out the complete set of changes to an app, check out the example app.