scgough
scgough

Reputation: 5252

React-Native Android ReactContext returning null

As the React-Native AppState API is iOS only I'm writing an appState event emitter for the Android side of my app. In the app's MainActivity on the Java side, I want to emit from the onResume and onPause functions to tell the JS side over the bridge that my app is either in the foreground or background

At the moment, I can minimise my app (and go back to the home-screen on my device) and the background event is emitted correctly. However, when I resume my app nothing is fired...also nothing is fired when the app initially opens up.

I've narrowed this down to the fact that in these cases mReactInstanceManager.getCurrentReactContext() is null...for some reason.

Here's my code from MainActivity.java:

@Override
protected void onPause() {
    super.onPause();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onPause();

        //send the appState back to JS
        Log.d("REACT_STATE", "Paused");  //this always fires
        if(mReactInstanceManager.getCurrentReactContext()!=null) {
            WritableMap params = Arguments.createMap();
            params.putString("currentAppState", "background");
            sendEvent(mReactInstanceManager.getCurrentReactContext(), "appStateChange", params);
        }
    }
}

and,

@Override
protected void onResume() {
    super.onResume();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onResume(this, this);

        //send the appState back to JS
        Log.d("REACT_STATE", "Resumed"); //this also always fires
        if(mReactInstanceManager.getCurrentReactContext()!=null) {
            WritableMap params = Arguments.createMap();
            params.putString("currentAppState", "foreground");
            sendEvent(mReactInstanceManager.getCurrentReactContext(), "appStateChange", params);
        }
    }
}

and this is the emitter code:

private void sendEvent(ReactContext reactContext,
                       String eventName,
                       @Nullable WritableMap params) {
    reactContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(eventName, params);
}

The JS side of things is set up OK and working - I can provide this code if it helps. I'm fairly certain my issue lies on this side. Am I trying to emit this at the wrong time? Do I need to move it elsewhere? Any advice is much appreciated!

Upvotes: 4

Views: 5625

Answers (3)

Program-Me-Rev
Program-Me-Rev

Reputation: 6624

GitHub: Is there any callback for android when ReactContext is created in MainAcitivity? #3887

You could avoid this by listening to the onReactContextInitialized callback.

@Override
public void onResume() {
    super.onResume();
    getReactInstanceManager().addReactInstanceEventListener(this);
}  

@Override
public void onPause() {
    super.onPause();
    getReactInstanceManager().removeReactInstanceEventListener(this);
}  

When your ReactContext is created, the callback will be notified:

@Override
public void onReactContextInitialized(ReactContext context) {
    Log.d(TAG, "Here's your valid ReactContext");
}  

OR:

reactInstanceManager.addReactInstanceEventListener(context -> {
    // first, grab the current context
    String eventName = "RevLoadPluginModulePathsEvent";

    // a WritableMap is the equivalent to a JS Object:
    // the React native bridge will convert it as is
    WritableMap params = Arguments.createMap();
    params.putString("MyCustomEventParam", "REV LOAD MODULES . . . .");

    revInitReactActivity.revSendReactEvent(context, eventName, params);
});  

Upvotes: 1

Jin Zhao
Jin Zhao

Reputation: 57

For anyone looking for ways to get the ReactContext instance from Activity, using

 this.getReactNativeHost().getReactInstanceManager().getCurrentReactContext();

instead of

 this.getReactInstanceManager().getCurrentReactContext();

Although I don't understand why...

Upvotes: 0

scgough
scgough

Reputation: 5252

Ok, I've managed to sort this. I created a new Plugin Package AppStateAndroid which emits back to the JS. This is added at the right time in the app lifecycle and works perfectly. I'm going to post the code up to GitHub (http://github.com/scgough) but for anyone that's wondering here you go:

public class AppStateAndroidPlugin extends ReactContextBaseJavaModule implements Application.ActivityLifecycleCallbacks {

private static final String PLUGIN_NAME = "AppStateAndroid";

private ReactContext mReactContext;

protected Activity activity = null;

protected Activity getActivity(){
    return this.activity;
}

public AppStateAndroidPlugin(ReactApplicationContext reactContext, Activity activity) {
    super(reactContext);

    this.mReactContext = reactContext;

    this.activity = activity;
    this.activity.getApplication().registerActivityLifecycleCallbacks(this);
}

private void sendEvent(ReactContext reactContext,
                       String eventName,
                       @Nullable WritableMap params) {

    Log.d(PLUGIN_NAME, "Sending Event"+params.toString());
    reactContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(eventName, params);
}
...
@Override
public void onActivityResumed(Activity activity) {
    Log.d(PLUGIN_NAME, "Resumed");
    if(mReactContext!=null) {
        WritableMap params = Arguments.createMap();
        params.putString("currentAppState", "active");
        sendEvent(mReactContext, "appStateChange", params);
    }
}

@Override
public void onActivityPaused(Activity activity) {
    Log.d(PLUGIN_NAME, "Paused");
    if(mReactContext!=null) {
        WritableMap params = Arguments.createMap();
        params.putString("currentAppState", "background");
        sendEvent(mReactContext, "appStateChange", params);
    }
}
...
@Override
public void onActivityDestroyed(Activity activity) {
    Activity myActivity = this.getActivity();
    if (activity == myActivity){
        myActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
    }
}

@Override
public String getName() {
    return PLUGIN_NAME;
}
}

You'll need a package file for this then in your main activity:

mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(getApplication())
            .setBundleAssetName("index.android.bundle")
            .setJSMainModuleName("index.android")
            .addPackage(new MainReactPackage())
            .addPackage(new AppStateAndroidPluginPackage(this))
            ...

Upvotes: 2

Related Questions