Putting native in React Native on Android

brightdevs

bright inventions

Posted on October 25, 2019

Putting native in React Native on Android

Using custom native components in React Native is a common thing, so sooner or later you may have to write some functionality in a native language and use it in your application. Let me show you a simple example how to do that.

First prepare code in a separate application

We start with creating a simple native application. In this example, our app will show the user information when the headset is plugged in or out. It involves several native interactions:

  • app lifecycle interaction while registering or unregistering listeners
  • sending and receiving data via intents
  • showing information by toasts
  • using application context

Assuming you are familiar with creating native Android applications, the example below will be very easy. Our app uses one activity with a simple layout, that later is going to be irrelevant. Start with the private fields that will be used with our activity:

private Context mContext;

private final BroadcastReceiver mHeadsetPlugReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
            boolean plugged = (intent.getIntExtra("state", 0) == 1);
            String message = plugged ? "Headset plugged in" : "Headset plugged out";
            Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
        }
    }
};

Remembering a reference to your application context is not always necessary, as long as we can call getApplicationContext method within our Activity. Let's keep it now because we will modify it later.

BroadcastReceiver is an abstract class and describes behavior on receiving information via Intent.

Registering listener looks like this:

private void registerBroadcastReceiver() {
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_HEADSET_PLUG);
    registerReceiver(mHeadsetPlugReceiver, filter);
}

registerReceiver method is an Activity's method and may be invoked on Context class object.

In onCreate method save the reference to the application context and register broadcast receiver:

mContext = this;
registerBroadcastReceiver();

In the end, following clean coding principles, unregister receiver when closing your application using Activity's unregisterReceiver method:

@Override
protected void onDestroy() {
    unregisterReceiver(mHeadsetPlugReceiver);
    super.onDestroy();
}

That's it, that is what we're going to work on. The app shows a simple message while plugging the headset in:

Let's use it in our RN project!

/react-project/android

In your React project directory, there is an android folder. Its structure looks like every Android project and you may open it with Android Studio for convenient navigation. Source files are under /app/src/main/java/{some}/{packages}/ and here we will add our code. We will have to pack our functionality in a specific way. Check MainApplication.java file first. It's extending Application class and implements ReactApplication interface. Take a look at getPackages method:

@Override
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage()
  );
}

An application written in React Native is built like any other Android application, but React Native packages are added at runtime and it is specified here. You may write any native code and link it here, but you have to add it as ReactPackage and initialize it in getPackages method. Will need two files: package and module.

First create a package file that implements com.facebook.react.ReactPackage interface. It's got two methods and its basic implementation looks like this:

public class MyHeadsetLibPackager implements ReactPackage {

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

        modules.add(new MyHeadsetLibModule(reactContext));

        return modules;
    }

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

The most important part is to initialize module in createNativeModules method:

modules.add(new MyHeadsetLibModule(reactContext));

MyHeadsetLibModule is how we name our second class. It will contain all functionalities of our library. It is necessary to extend com.facebook.react.bridge.ReactContextBaseJavaModule class for that.


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

public class MyHeadsetLibModule extends ReactContextBaseJavaModule {

    public MyHeadsetLibModule(ReactApplicationContext reactContext) {

        super(reactContext);

        /*
            Our starting point.
        */

    }
}

Last thing necessary to compile a project is to add dependency to /app/build.gradle

dependencies {
    ...
    compile "com.facebook.react:react-native:+"
    ...
}

Moving from activity

Now we can implement everything as we did in our activity. Just mind two consequences of moving from Activity to ReactContextBaseJavaModule.

First:

Who's got the context?

From now we cannot call activity's methods like registerReceiver just like that. We also cannot access the application's context by calling getApplicationContext. That's why, in the module's constructor we get ReactContext instance. All activity's methods will be called from it.

Second:

Where are the lifecycle methods?

Right now nowhere. But just implement LifecycleEventListener in your module class. It's an interface that provides three basic lifecycle methods:

  • void onHostResume()
  • void onHostPause()
  • void onHostDestroy()

All lifecycle functionalities implement here. Then in the constructor register listener with reactContext.addLifecycleEventListener(this) and... done. Our module behaves like activity now.

The last thing to make our module visible is to override getName method. It should be returning the name of our module, like that:

@Override
public String getName() {
    return "MyHeadsetLibModule";
}

Following all these guidelines a final form of our module rewritten from activity looks like this:


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.Toast;

import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

public class MyHeadsetLibModule extends ReactContextBaseJavaModule implements LifecycleEventListener {

    private ReactApplicationContext mContext;

    private final BroadcastReceiver mHeadsetPlugReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
                boolean plugged = (intent.getIntExtra("state", 0) == 1);
                String message = plugged ? "Headset plugged in" : "Headset plugged out";
                Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
            }
        }
    };

    public MyHeadsetLibModule(ReactApplicationContext reactContext) {
        super(reactContext);
        mContext = reactContext;
        registerBroadcastReceiver();
    }

    private void registerBroadcastReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_HEADSET_PLUG);
        mContext.registerReceiver(mHeadsetPlugReceiver, filter);
    }

    @Override
    public String getName() {
        return "MyHeadsetLibModule";
    }

    @Override
    public void onHostResume() {

    }

    @Override
    public void onHostPause() {

    }

    @Override
    public void onHostDestroy() {
        mContext.unregisterReceiver(mHeadsetPlugReceiver);
    }    
}


Exposing methods to JS

What if we would like to register BroadcastReceiver not on the application start, but later, and invoke it from React Native module in TypeScript? Here comes @ReactMehod annotation. Just add a method with it to your module class:

@ReactMethod
public void startTrackingAudioJackPlug() {
    registerBroadcastReceiver();
}

Now in any TypeScript file, we can import it from react-native:

import { NativeModules } from 'react-native'

and use calling it directly from NativeModules object:

NativeModules.MyHeadsetLibModule.startTrackingAudioJackPlug()

Getting callback from native module

Last modification - let the message about plugging headset will be displayed not by native toast, but some React Native alert. To do so we have to emit information about plugging headset from native module to JS module. In native code add this:

reactContext
    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit(eventName, params);
  1. eventName is String object and identifies emitted message
  2. params is WritableMap object and contains parameters in simple key-value collection

Now just register listener in JS module to receive the emitted message. Import DeviceEventEmitter from 'react-native' and add this code in componentDidMount method:

DeviceEventEmitter.addListener('CustomNameOfTheEvent', function(e: Event) {

    /*
        Here you can display an alert.
        Get parameters values from event like this:
        let parameter = e["key"]
    */

})

That's it. We have just implemented a native functionality with bidirectional communication.

💖 💪 🙅 🚩
brightdevs
bright inventions

Posted on October 25, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related