How to create an unstoppable service in React Native using Headless JS (Android)
Mathias Silva da Rosa
Posted on May 18, 2019
If you have already developed a mobile application with a relative complexity, probably you found yourself breaking your head out with some tasks that are not so common. This already happened to me in the development of our app at Segware integrating events of a specific hardware. The challenge was that every event should be processed by the app regardless the state of it, i.e, with the app in foreground, background, closed or even when it was not started (device reboots).
In this case, it's necessary to have something not attached to the application but in the same time it must be unstoppable, alive almost one hundred percent of the time to execute the tasks. In a native android application, it's could be done with the use of a component called Service that is able to execute tasks without an UI thread or a user action.
The main goal of this article is to explain how to implement a native android module to execute this kind of task in a React Native (RN) application, with the use of services, broadcast receivers and headless JS. In this article I used as example a simple heartbeat application and I will show how I created it step by step.
Step 1 - Creating the Bridge
For a full understanding, I will present a tiny brief of how to implement a bridge to a native module in RN. If you like automatic process or want an easier way to do that I recommend using react-native-create-bridge or if you very familiar with this step, you can skip right to step 2.
In the actual context, a bridge is a means of communication between the RN layer and the native layer. Just as the documentation says, to create a bridge, firstly you must provide a Package responsible to register your module or UI module (not used here) in the RN layer. As shown in the class below, it's instantiated the module passing the react context as a parameter.
// HeartbeatPackage.java
public class HeartbeatPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
new HeartbeatModule(reactContext)
);
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
The Module itself is responsible for defining the methods and props that will be available to the RN layer in the native layer. To expose a Java method, it must be annotated using @ReactMethod and the return will be always void. The bridge is asynchronous, so the only way to pass a result to RN layer is by using callbacks or emitting events.
// HeartbeatModule.java
public class HeartbeatModule extends ReactContextBaseJavaModule {
public static final String REACT_CLASS = "Heartbeat";
private static ReactApplicationContext reactContext;
public HeartbeatModule(@Nonnull ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Nonnull
@Override
public String getName() {
return REACT_CLASS;
}
@ReactMethod
public void startService() {
// Starting the heartbeat service
this.reactContext.startService(new Intent(this.reactContext, HeartbeatService.class));
}
}
To finish the bridge, it's just needed to connect it to the two sides. In the MainApplicaiton.java, automatically created with the RN project in the android folder, you must instantiate the new module in the getPackages method. Once you have done so, the methods and props of the module will be available and you can import and use then like below.
// Heartbeat.js
import { NativeModules } from 'react-native';
const { Heartbeat } = NativeModules;
export default Heartbeat;
Important: The module's name must be the same defined by the REACT_CLASS variable in module class.
Step 2 - Creating the Service
As commented before, a service is used to execute tasks detached from the main thread of the application. In this example, the created service is responsible, when it's started, to send heartbeats events in a specific interval of time. There are some ways to implement an interval in android, but here I used a Handler to process a runnable object and send the event every 2 seconds. The RCTDeviceEventEmitter can signal events to RN layer and it can be obtained directly from the react context, like below.
public class HeartbeatService extends Service {
...
private Handler handler = new Handler();
private Runnable runnableCode = new Runnable() {
@Override
public void run() {
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("Heartbeat",null);
handler.postDelayed(this, 2000);
}
};
...
}
The documentation of Android Services is very clear about how would be the impact of the android's system manager on your service. If it finds out that your device is in critical operation like low memory or low battery, it will kill some services to free these resources. To prevent this from happening, you have to turn your service to a Foreground kind and to do so a notification must be fixed on the status bar of the device. With this, the notification were included in the onStartMethod of the service class, like below.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
this.handler.post(this.runnableCode); // Starting the interval
// Turning into a foreground service
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("Heartbeat service")
.setContentText("Running...")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(contentIntent)
.setOngoing(true)
.build();
startForeground(SERVICE_NOTIFICATION_ID, notification);
return START_STICKY;
}
Important: For the service to work, it's also necessary to declare it in the AndrodManifest.xml. See this.
Step 3 - Creating the BroadcastReceiver
A BroadcastReceiver, as the name itself says, it's a responsive component to broadcast messages from the android system or from another apps, similar to the publish-subscribe design pattern. There are many broadcast messages available in an android system, but in our case the most important one is the BOOT_COMPLETED. It tells us that that the device suffered a reboot operation and this process is finished.
Listening to this specific message, our broadcast receiver will be able to restart the service created before some seconds after the device reboot process finishes, which will keep sending the heartbeat events. The implemented code is shown below.
// BootUpReceiver.java
public class BootUpReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
context.startService(new Intent(context, HeartbeatService.class));
}
}
In the AndrodManifest.xml, it's also needed to declare the broadcast receiver and define the message that it will be listening. The corresponding permissions to read this kind of message must be provided as well.
<manifest...>
...
android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...
<application...>
...
<receiver
android:name="com.rnheartbeat.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>
</application>
</manifest>
Step 4 - Creating the Application
Certainly, this is the simplest step of this tutorial but in the same time it's the most exciting one. It shows the result of everything done so far. In the RN layer, we need implement a component to interact and shows to the user the heartbeats events and I thought that the most suitable example to this event will be a heart pulse action, so let's do it.
In the App.js of the RN project I created a simple component as you can see below. With the benefits provided by the useState and useEffect of React Hooks, a listener is registered and every heartbeat event will change the width and height of a heart image. Simple enough, right? Moreover, the press of a button component calls the startService method from the native layer.
const App = () => {
const [heartBeat, setHeartBeat] = useState(false);
useEffect(() => {
DeviceEventEmitter.addListener('HeartBeat', () => {
console.log('Receiving heartbeat event');
setHeartBeat(true);
setTimeout(() => {
setHeartBeat(false);
}, 1000);
});
});
const imageSize = heartBeat ? 150 : 100;
return (
<View style={styles.container}>
<View style={styles.view}>
<Image source={heart} style={{ width: imageSize, height: imageSize }} resizeMode="contain" />
</View>
<View style={styles.view}>
<TouchableOpacity style={styles.button} onPress={() => Heartbeat.startService()}>
<Text style={styles.instructions}>Start</Text>
</TouchableOpacity>
</View>
</View>
);
};
Finally, this is the time to test our application. As commented in the beginning of this article, the app to be approved in our test, it must to process the events in its 4 states (foreground, background, closed or not started). In the first state, when the app is shown to the user, it performs like the gif below. When the service is started a notification is pop up in the status bar of the device.
Probably, you are wondering about how we could see the processing of the event in the other states of the app. That's when we run the command react-native log-android in the root folder of the project. Running this in a command line we can se the console.log executed by the service when the app is in background or closed, like below.
Ok, now we see that the application in doing well, but the test when the app is not started is missing. We expect that the broadcast receiver will start the service again and it will continue generating the heartbeat events to the RN layer and we can see the console.log messages again. But for our surprise it does not happen. Frustrating right?
When the device reboots and the broadcast message is sent, the service created in the the native layer gets up and start to send the events. However, there is no action to also gets up the RN layer to receive these events. In this moment we have a NullPointerException when the service tries to access a nonexistent React Context. To solve this problem it's time to use a great tool provided by the React API: Headless JS.
Step 5 - Creating the Headless Service
A Headless JS is a RN service able to encapsulate some javascript operations and execute them detached from the application, like an android service. The headless service can do any kind of operation, except UI operations. In the actual example, the headless service can substitute the RCTDeviceEventEmitter, removing the direct use of React Context in the native layer.
Firstly, in the HeartbeatService class, we replace the event emitter with the headless service start, like below. When we do it, the service will execute the operation and it will go into a "paused" mode, until it is started again after two seconds.
public class HeartbeatService extends Service {
...
private Handler handler = new Handler();
private Runnable runnableCode = new Runnable() {
@Override
public void run() {
// context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("Heartbeat",null);
Context context = getApplicationContext();
Intent myIntent = new Intent(context, HeartbeatEventService.class);
context.startService(myIntent);
HeadlessJsTaskService.acquireWakeLockNow(context);
handler.postDelayed(this, 2000);
}
};
...
}
Now we create the HeartbeatEventService like below. You can notice that we don't use the React Context, so the service has its own context. The name of the event in the settings must be the same defined before.
//HeartbeatEventService
public class HeartbeatEventService extends HeadlessJsTaskService {
@Nullable
protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Bundle extras = intent.getExtras();
return new HeadlessJsTaskConfig(
"Heartbeat",
extras != null ? Arguments.fromBundle(extras) : null,
5000,
true);
}
}
A headless service must be registered on AppRegistry and just as its documentation says, every app registry should be required early in the require sequence to make sure the JS execution environment is setup before other modules are required. In the index.js we call the registry like below.
// index.js
AppRegistry.registerHeadlessTask('Heartbeat', () => MyHeadlessTask);
Now, the MyHeadlessTask is responsible to change the width and height of the heart image through a state. As we have here a centralized state shared with the App component, I decided to use Redux instead of hooks. With this, it's just necessary to create the basic structure of redux (action, reducer and store) to set properly the state. The MyHeadlessTask method is shown below.
const MyHeadlessTask = async () => {
console.log('Receiving HeartBeat!');
store.dispatch(setHeartBeat(true));
setTimeout(() => {
store.dispatch(setHeartBeat(false));
}, 1000);
};
When a heartbeat is fired, the state passed to the App changes, rerendering the whole component. If we test again with the reboot process we can see that even in this state the service is able to gets up and fires the event again. Oh yeah! That's how it's done.
Conclusion
We have done so far an unstoppable service in React Native integrating a native android module and using some important tools like broadcast receivers, headless JS and much more. For future works, the challenge is to do the same for an iOS system that has some restrictions about to do tasks with the app closed or not started.
All the code of this example you can see in
mathias5r/rn-heartbeat
Posted on May 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 18, 2019