Android | iOS |
---|---|
This sample, which grew out of a question on Stack Overflow, demonstrates the interface between React Native JavaScript and native code – Java on Android, Objective-C on iOS.
The original version was Android-only; support for iOS was added in March 2019.
This project demonstrates the following:
ActivityStarter
:
Clipboard
, which comes with React Native out of the box:
TextInput
. (Android only.)There is no technical difference between the ActivityStarter
and Clipboard
native modules, except one is defined in this project while the other ships as part of React Native.
The starting point for this sample is a slightly tweaked standard React Native project as generated by a long-outdated version of react-native init
. We add six buttons to the generated page:
The TextInput
box appears only in the Android version. Since both platforms use the same JavaScript, I took the opportunity to demonstrate how to handle platform-specific tweaks – look for Platform.select
in index.js
.
git clone https://github.com/petterh/react-native-android-activity.git
cd react-native-android-activity
yarn
to download dependencies (or, if you wish, npm install
)yarn start
adb reverse tcp:8081 tcp:8081
bundleInDebug=true
in android/gradle.properties
.open Activity.xcworkspace
.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.js
</Text>
<!-- Menu buttons: https://facebook.github.io/react-native/docs/debugging -->
<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);
ReactNativeHost
whose getPackages
include our package in its list.ActivityStarterReactPackage
includes ActivityStarterModule
in its native modules list.ActivityStarterModule
returns “ActivityStarter” from its getName
method, and annotates three methods with the ReactMethod
attribute.ActivityStarter.getActivityName
and friends via NativeModules
.The iOS Objective-C classes are parallel to the Android Java classes. There are differences:
AppDelegate
class.DeviceEventManagerModule.RCTDeviceEventEmitter
and fire away; in iOS it is necessary to subclass RCTEventEmitter
.Here is a sample of an Objective-C class implementation with methods callable from JavaScript:
@implementation ActivityStarterModule
RCT_EXPORT_MODULE(ActivityStarter);
RCT_EXPORT_METHOD(navigateToExample)
{
dispatch_async(dispatch_get_main_queue(), ^{
AppDelegate *appDelegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[appDelegate navigateToExampleView];
});
}
RCT_EXPORT_METHOD(getActivityName:(RCTResponseSenderBlock) callback)
{
callback(@[@"ActivityStarter (callback)"]);
}
@end
This requires the react native bridge, so responsibility resides with the AppDelegate
class, for convenience.
- (void) callJavaScript
{
[self.reactBridge enqueueJSCall:@"JavaScriptVisibleToJava"
method:@"alert"
args:@[@"Hello, JavaScript!"]
completion:nil];
}
I just added a second version of ActivityStarterModule.getActivityName
called getActivityNameAsPromise
, with a corresponding button.
I added a sample of event triggering, another way to communicate. Tap Start Example Activity, then Trigger event.
The various Android apps explicitly call SoLoader.init
because of this issue. I have a PR to fix it. Once this is in (assuming Facebook accepts it) I’ll remove them.