This sample demonstrates the interface between React Native JavaScript and Java code in the Android host application. It can do the following:
- JavaScript calling into native modules:
- Navigate from React Native to a Java activity internal to the host app;
- Start an external intent (dial a phone number), passing data from JavaScript;
- Query the host app for information;
- Copy information to the clipboard.
- Java calling into JavaScript
The first three use a custom native module called ActivityStarter
; the fourth uses the native module Clipboard
, which comes with React Native out of the box. Technically there is no difference. The final one invokes ActivityStarter.callJavaScript
, which in turn calls a JavaScript method from Java.
The starting point for this sample is a slightly tweaked standard RN project as generated by react-native init
. We add five buttons to the generated page:
- Install Git.
- Install Node.js. Use a shell with Node and Git in the path (or the Node.js shell itself) for all commands.
- Clone this project: git clone https://github.com/petterh/react-native-android-activity.git
- cd react-native-android-activity
- Update
npm
: npm install -g npm - Run npm install to download dependencies
- Install Android Studio (follow instructions on this page).
- By default, the debug build of the app loads the JS bundle from your dev box, so start a packager:
npm run start
- Connect an Android device via USB, or use an emulator.
- Open the app in Android Studio and run it.
- If this fails with the message "Could not get BatchedBridge, make sure your bundle is packaged correctly", your packager is likely not running.
- If it complains about connecting to the dev server, run adb reverse tcp:8081 tcp:8081
- If it crashes while opening the ReactNative controls, try to modify the following phone settings: Android Settings -> Apps -> Settings once again (the gear) to go to Configure Apps view -> Draw over other apps -> Allow React Native Android Activity Demo to draw over other apps. (The demo app should ask for this automatically, though.)
- To embed the bundle in the apk (and not have to run the packager), do two changes:
- In
MainApplication
, makegetUseDeveloperSupport
returnfalse
. - In
app/build.gradle
, setbundleInDebug: true
.
- In
The gist of the JavaScript code looks like this:
import { ..., NativeModules, ... } from 'react-native';
export default class ActivityDemoComponent extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Text style={styles.instructions}>
Double tap R on your keyboard to reload,{'\n'}
Shake or press menu button for dev menu
</Text>
<View style={styles.buttonContainer}>
<Button
onPress={() => NativeModules.ActivityStarter.navigateToExample()}
title='Start example activity'
/>
<Button
onPress={() => NativeModules.ActivityStarter.dialNumber('+1 (234) 567-8910')}
title='Dial +1 (234) 567-8910'
/>
<Button
onPress={() => NativeModules.ActivityStarter.getName((name) => { alert(name); })}
title='Get activity name'
/>
<Button
onPress={() => NativeModules.Clipboard.setString("Hello from JavaScript!")}
title='Copy to clipboard'
/>
</View>
</View>
);
}
}
The first three buttons use three methods on NativeModules.ActivityStarter
. Where does this come from?
ActivityStarter
is just a Java class that implements a React Native Java interface called NativeModule
. The heavy lifting of this interface is already done by BaseJavaModule
, so one normally extends either that one or ReactContextBaseJavaModule
:
class ActivityStarterModule extends ReactContextBaseJavaModule {
ActivityStarterModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "ActivityStarter";
}
@ReactMethod
void navigateToExample() {
ReactApplicationContext context = getReactApplicationContext();
Intent intent = new Intent(context, ExampleActivity.class);
context.startActivity(intent);
}
@ReactMethod
void dialNumber(@Nonnull String number) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number));
getReactApplicationContext().startActivity(intent);
}
@ReactMethod
void getActivityName(@Nonnull Callback callback) {
Activity activity = getCurrentActivity();
if (activity != null) {
callback.invoke(activity.getClass().getSimpleName());
}
}
}
The name of this class doesn't matter; the ActivityStarter
module name exposed to JavaScript comes from the getName()
method.
Each method annotated with a @ReactMethod
attribute is accessible from JavaSCript. Overloads are not allowed, though; you have to know the method signatures. (The out-of-the-box Clipboard
module isn't usually accessed the way I do it here; React Native includes Clipboard.js
, which makes the thing more accessible from JavaScript – if you're creating modules for public consumption, consider doing something similar.)
A @ReactMethod
must be of type void
. In the case of getActivityName()
we want to return a string; we do this by using a callback.
The default app generated by react-native init
contains a MainApplication
class that initializes React Native. Among other things it extends ReactNativeHost
to override its getPackages
method:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
This is the point where we hook our Java code to the React Native machinery. Create a class that implements ReactPackage
and override createNativeModules
:
class ActivityStarterReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ActivityStarterModule(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Finally, update MainApplication
to include our new package:
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new ActivityStarterReactPackage(), // This is it!
new MainReactPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
}
}
This demo is invoked by the last button on the page:
<Button
onPress={() => NativeModules.ActivityStarter.callJavaScript()}
title='Call JavaScript from Java'
/>
The Java side looks like this (in ActivityStarterReactPackage
class):
@ReactMethod
void callJavaScript() {
Activity activity = getCurrentActivity();
if (activity != null) {
MainApplication application = (MainApplication) activity.getApplication();
ReactNativeHost reactNativeHost = application.getReactNativeHost();
ReactInstanceManager reactInstanceManager = reactNativeHost.getReactInstanceManager();
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
CatalystInstance catalystInstance = reactContext.getCatalystInstance();
WritableNativeArray params = new WritableNativeArray();
params.pushString("Hello, JavaScript!");
catalystInstance.callFunction("JavaScriptVisibleToJava", "alert", params);
}
}
}
The JavaScript method we're calling is defined and made visible to Java as follows:
import BatchedBridge from "react-native/Libraries/BatchedBridge/BatchedBridge";
export class ExposedToJava {
alert(message) {
alert(message);
}
}
const exposedToJava = new ExposedToJava();
BatchedBridge.registerCallableModule("JavaScriptVisibleToJava", exposedToJava);
- The main application class initializes React Native and creates a
ReactNativeHost
whosegetPackages
include our package in its list. ActivityStarterReactPackage
includesActivityStarterModule
in its native modules list.ActivityStarterModule
returns "ActivityStarter" from itsgetName
method, and annotates three methods with theReactMethod
attribute.- JavaScript can access
ActivityStarter.getActivityName
and friends viaNativeModules
.
I just added a second version of ActivityStarterModile.getActivityName
called getActivityNameAsPromise
, with a corresponding button.
I just added event triggering, another way to communicate. Tap Start Example Activity, then Trigger event.
This is primarily an Android sample – but if I can lay my hands on a Mac, or enlist the help of someone with a Mac, I'll add the corresponding functionality to the iOS version of the app.