React Native for Android: yet another Context

October 29, 2017

android
react native

If you have to inject / create a React Native module into your Android Activity (that does not extend the default React Native Activity), please beware that the Activity lifecycle is not tied to the React Context lifecycle used to create the RN module.

So you have to be careful to retrieve it at the right moment, bearing in mind that a developer can hot reload (pressing double R on the simulator), causing the creation of a new React Context.

Also, the framework gives you a way to listen to the lifecycle of the topmost React Native Acitivity, so that you don’t need to hold a reference to it.

To show how to solve these things, I wrote a short example that you can find here.

Please bear in mind that the Javascript part is very dumb. The main point is the Native / Java (yeah, no Kotlin yet for me) part.

Here is what the running app look like:

React Native for Android example

To use React Native in Android, we have to create a ReactNativeHost that contains a ReactInstanceManager through which we can retrieve the ReactContext.

Here I presume that you have only one React Native Instance / CatalystInstance per Application.

This seems to be the correct way most of the time, because, apart from performances reasons, if you create a standard React Native sample app, it will declare one ReactNativeHost in the Application itself (:P).

That same React Native Instance Manager can also be used to register additional RN packages, via ReactNativeInstanceManager.registerAdditionalPackages(List<ReactPackage>), so that we can decouple the registering of RN packages from the creation of the ReactNativeHost.

Why do we need a ReactContext?

A ReactContext contains methods that can be useful, like the one that allows you to retrieve a React Native Module, so that you can send an event to JS land.

We may also need to listen to the lifecycle of the topmost React Native Activity that we have.

That is again achieved via the ReactContext.

How do we retrieve an instance of ReactContext?

At the time of writing, there is a ReactInstanceManager.getCurrentReactContext() but it seems to be VisibleForTesting.

So we need to find another way.

The main entry point is the callback onReactContextInitialized(ReactContext context) of ReactInstanceEventListener.

We simply have to add and remove the listener from the instance manager accordingly, whenever we need a ReactContext.

Run the sample and tap on the button to open the RN activity:

open_react_native_activity_button

In ReactNativeActivity.onResume() we add the listener.

When the ReactNativeActivity is opened, if we hot reload (keyboard ‘RR’) the callback is triggered and in ReactNativeActivity.onReactContextInitialized() we get our new ReactContext.

In ReactNativeActivity.onPause() we remove the listener.

Since the ReactContext can be recreated with the hot reload and since it can be created in background, its lifecycle is not tied to the lifecycle of the Activity.

If we put breakpoints in all the mentioned places, we should see that the first time we open the ReactNativeActivity, we don’t receive the new ReactContext in the callback. That is because by the time onResume() is called, the new ReactContext might have already been created via ReactActiveDelegate.loadApp(String).

Also, if we close the ReactNativeActivity and open it again, we lose the ReactContext instance.

To solve this, I made the Application to implement ReactInstanceEventListener.onReactContextInitialized(ReactContext) too, I register the listener in MainApplication.onCreate() and in the callback I store an instance of ReactContext.

Then in ReactNativeActivity.onCreate(Bundle) I simply retrieve it via getApplication().

With some breakpoints we can confirm that the ReactApplicationContext that we receive when we open the Activity, is the same both in the Activity:

activity_reactcontext_open

And in the Application:

application_reactcontext_open

Also if we hot reload during development, the ReactContext is updated correctly in both places:

Activity – Reload

activity_reactcontext_reload

Application – Reload

application_reactcontext_reload

How we obtain a reference to the topmost React Native Activity?

To do that, your class (for example a Native Module) has to implement a LifecycleEventListener and add / remove itself via ReactContext.addLifecycleEventListener(LifecycleEventListener) and ReactContext.removeLifecycleEventListener(LifecycleEventListener).

Then in onHostResume() / onHostPause() / onHostDestroy() you can obtain the topmost React Native Activity via getCurrentActivity().

You can find an example in my ActivityCounterModule.

Note that, as stated in the javadoc of LifecycleEventListener, only the topmost React Native Activity is considered, so we will never receive a onHostDestroy() callback from an Activity that has been paused.