React Native Android Background Service
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
- How to create an unstoppable service in React Native using Headless JS
- HeadlessJS
- React Native Android native modules