In this post:

Aim and overview

This is a detailed re-hash of [this article] on how to create a persistent background service in the Java native layer which is able to interact with the React Native layer — the React Native Bridge.

If a Service is programmed in the Java native layer while running, it can, for example, undertake tasks at specified intervals or respond to an intent and be “hooked in” to communicate with the React Native layer via the AppRegistry.registerHeadlessTask() function known as HeadlessJS.

Structure

To create the React Native Bridge, in the Java native layer the entry point is in the MainApplication class where a package needs to be included, or added, to the ReactPackage list. The ReactPackage class acts as a wrapper to invoke a ReactContextBaseJavaModule class to register a module in the React Native layer which provides the functionality to start and stop the background service.

Additional Java class files

Under android/app/src/main/java/com/$APP_NAME four Java class files need to be created and have the following relationship:

    ,-------------------.        ,-------------------.  
    |  MainApplication  |  <-    |  ReminderPackage  |   
    `-------------------´        `-------------------´  
                                           ^
                                           | 
                                  ,------------------.
                                  |  ReminderModule  |
                                  `------------------´
                                           ^
                                           |
                                        starts
                                           | 
  ,------------------.            ,-------------------.
  |  BootUpReceiver  | <- starts  |  ReminderService  |
  `------------------´            `-------------------´
                                           ^
                                           |
                               sends events to RN layer via
                                           | 
                               ,------------------------.
                               |  ReminderEventService  |
                               `------------------------´

Step 1: Package class

For example, to add a new package called ReminderPackage, in the main body of the MainApplication class declaration:

private final ReactNativeHost mReactNativeHost =
    new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            @SuppressWarnings("UnnecessaryLocalVariable")
            List<ReactPackage> packages = new PackageList(this).getPackages();
            // Packages that cannot be autolinked yet can be added manually here, for example:
            // packages.add(new MyReactNativePackage());
            packages.add(new ReminderPackage()); // new package added
            return packages;
        }

and some boilerplate class code needs to be created in the file ReminderPackage.java with the required module (ReminderModule) being instantiated with reactContext being passed as parameter:

package %PACKAGE_NAME%;

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.Arrays;
import java.util.Collections;
import java.util.List;

public class ReminderPackage implements ReactPackage {

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Arrays.<NativeModule>asList(new ReminderModule(reactContext));
    }

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

Step 2: Module class

The class code for the ReminderModule referenced above defines what will be available to the React Native layer. Each method annotated with @ReactMethod will be exposed to the React Native layer.

The REACT_CLASS returned by the getName() method is essential to link the bridge and is used on the React Native layer side to access the exposed methods.

Note that communication with the React Native layer is asynchronous and only available by emitting events or via a callback, and an exposed method always returns void.

The following is a basis boilerplate:

package %PACKAGE_NAME%;

import android.content.Intent;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import javax.annotation.Nonnull;

public class ReminderModule extends ReactContextBaseJavaModule {

    public static final String REACT_CLASS = "Reminder";
    private static ReactApplicationContext reactContext;

    public ReminderModule(@Nonnull ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @NonNull
    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @ReactMethod
    public void startService() {
        // Starting the reminder service
        this.reactContext.startService(new Intent(this.reactContext, ReminderService.class));
    }
    
    @ReactMethod
    public void stopService() {
        this.reactContext.stopService(new Intent(this.reactContext, ReminderService.class));
    }
}

Step 3: Service class

The ReminderService class extends Service and this is where tasks separate from the main thread can be run, for example at a set interval. This is where a task can send events to the React Native layer via ReminderServiceEvent class (still to be created). This will in fact create a foreground service and the notification drawer in status area of the device. The code responsible for this is in the onStartCommand() method below.

Note: The service needs to be declared in the AndroidManifest.xml thus:

<service
    android:name=".ReminderService"
    android:enabled="true"
    android:exported="true" />

Class code:

package %PACKAGE_NAME%;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import com.facebook.react.HeadlessJsTaskService;

public class ReminderService extends Service {

    private static final String CHANNEL_ID = "Reminder";
    private static final String NOTIF_TITLE = "Reminder Service";
    private static final int SERVICE_NOTIF_ID = 321;

    public ReminderService() {}

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    private Handler handler = new Handler();
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            Context context = getApplicationContext();
            Intent myIntent = new Intent(context, ReminderEventService.class);
            context.startService(myIntent);
            HeadlessJsTaskService.acquireWakeLockNow(context);
            handler.postDelayed(this, 2000);
        }
    };

    private void createNotificationChannel() {
        // Create notification channel for API 26+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "REMINDER", importance);
            channel.setDescription("CHANNEL DESCRIPTION");
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        this.handler.post(this.runnable);
        createNotificationChannel();
        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle(NOTIF_TITLE)
                .setContentText("Running...")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentIntent(contentIntent)
                .setOngoing(true)
                .build();
        startForeground(SERVICE_NOTIF_ID, notification);
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        this.handler.removeCallbacks(this.runnable);
    }
}

Step 4: EventService (HeadlessJS) class

This HeadlessJS service allows Javascript (except UI) operations to be run detached from the application. This method is an alternative to RCTDeviceEventEmitter, removing the need to access React Context in the native layer. This requires

<service
android:name=".ReminderEventService"/>

in the AndroidManifest.xml and the class code is as follows:

package %PACKAGE_NAME%;

import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;

public class ReminderEventService extends HeadlessJsTaskService {

    @Nullable
    protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
        Bundle extras = intent.getExtras();
        WritableMap data = extras != null ? Arguments.fromBundle(extras): Arguments.createMap();
        return new HeadlessJsTaskConfig(
            "ReminderService", /* MUST be the same as the Service class */
            data,
            5000, /* pause for 5 seconds */
            true);
    }
}

On the React Native side this headless service needs to be registered on AppRegistry, typically in the index.js file:

import { AppRegistry } from 'react-native'
import BackgroundTask from './BackgroundTask'
AppRegistry.registerHeadlessTask('ReminderService', () => BackgroundTask)

and in BackgroundTask.js:

module.exports = async (taskData) => {
    // do stuff, e.g. redux actions etc
}

Step 5: BroadcastReceiver class

The final component in the Java layer is the BroadcastReceiver responsible for ensuring the service is started on reboot.

package %PACKAGE_NAME%;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class BootUpReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        context.startService(new Intent(context, ReminderService.class));
    }
}

which requires in the AndroidManifest.xml the following at the top level:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

and inside the <application> tag:

<receiver
    android:name=".BootUpReceiver"
    android:enabled="true"
    android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</receiver>

Step 6: React Native interface to NativeModules

The methods defined and annotated with @ReactMethod in the module extending ReactContextBaseJavaModule (step 2, above) are available to the React Native layer and can be imported thus:

// Reminder.js
import { NativeModules } from 'react-native'
const { Reminder } = NativeModules /* must be same as REACT_CLASS variable in module class */

/* alternative 1 */
// export default Reminder

/* alternative 2 */
const wrapper = {
    startService() { Reminder.startService() },
    stopService() { Reminder.stopService() },
    /* any other methods similarly defined */
}

export default wrapper

This enables the reminder service to be started from the app, e.g. from a button or on mounting.

import Reminder from './Reminder'

<TouchableOpacity
    style={{ border: '1px solid #ddd', padding: 6, margin: 6, }}
    onPress={() => Reminder.startService()}
>
  <Text style={styles.instructions}>Start Reminder Service</Text>
</TouchableOpacity>

TODO

using RCTDeviceEventEmitter.

References