react-native: [Android] BackAndroid 'hardwareBackPress' event not working

BackAndroid imported with :

var {
  AppRegistry,
  StyleSheet,
  TouchableHighlight,
  Text,
  Navigator,
  View,
  ListView,
  ToolbarAndroid,
  BackAndroid,
  TextInput,
} = React;
BackAndroid.addEventListener('hardwareBackPress', function() {
  return true;
});

-> still quit.

On the docs, it says if it return true, app should not quit.

Here is an extremely minified version of my app : https://rnplay.org/apps/Ss8E8Q On the demo, it says “unfortunately, the app has stopped.” On my phone, it says nothing.

If it can helps: Tried using LG G2 on CloudyG2 with 5.0.2. Will try on Nexus 5 (5.1.1) and genymotion later.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 20 (3 by maintainers)

Most upvoted comments

Leaving for any posterity…

I am on v0.46.0 of react-native and had the same issue. I tracked the issue down to this file in the react-native code base

https://github.com/facebook/react-native/blob/master/Libraries/Utilities/BackHandler.android.js#L25

When running with the chrome debugger turned off the line

var subscriptions = Array.from(_backPressSubscriptions.values()).reverse()

always returns an empty array for subscriptions which in turn causes the invokeDefault variable to stay true and the .exitApp() function to be called.

After more investigation, I think the issue was discovered and discussed in the following PR https://github.com/facebook/react-native/pull/15182.

Even after copy/pasting the PR change in an older version of RN it did not work most likely caused by the issue described in the PR.

After some very slight modifications I got it working by changing to

RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
  var invokeDefault = true;
  var subscriptions = []
  _backPressSubscriptions.forEach(sub => subscriptions.push(sub))

  for (var i = 0; i < subscriptions.reverse().length; ++i) {
    if (subscriptions[i]()) {
      invokeDefault = false;
      break;
    }
  }

  if (invokeDefault) {
    BackHandler.exitApp();
  }
});

Simply using a .forEach which was the original implementation on the PR before the amended Array.from syntax works throughout.

So you could fork react-native and use a modified version, submit a PR though I imagine that will take a little while to be approved and merged upstream, or you can do something similar to what I did which was to override the RCTDeviceEventEmitter.addListener(...) for the hardwareBackPress event.

// other imports
import { BackHandler, DeviceEventEmitter } from 'react-native'

class MyApp extends Component {
  constructor(props) {
    super(props)
    this.backPressSubscriptions = new Set()
  }

  componentDidMount = () => {
    DeviceEventEmitter.removeAllListeners('hardwareBackPress')
    DeviceEventEmitter.addListener('hardwareBackPress', () => {
      let invokeDefault = true
      const subscriptions = []

      this.backPressSubscriptions.forEach(sub => subscriptions.push(sub))

      for (let i = 0; i < subscriptions.reverse().length; i += 1) {
        if (subscriptions[i]()) {
          invokeDefault = false
          break
        }
      }

      if (invokeDefault) {
        BackHandler.exitApp()
      }
    })

    this.backPressSubscriptions.add(this.handleHardwareBack)
  }

  componentWillUnmount = () => {
    DeviceEventEmitter.removeAllListeners('hardwareBackPress')
    this.backPressSubscriptions.clear()
  }

  handleHardwareBack = () => { /* do your thing */ }
  
  render() { return <YourApp /> }
}

I had to add the following to MainActivity.java,

@Override
public void onBackPressed() {
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onBackPressed();
    } else {
        super.onBackPressed();
    }
}

@austenLacy, you da bomb!

@austenLacy Your solution worked for me. Thank you.

@austenLacy your solution worked like a charm! thanks a ton for your great comment!

I cannot overstate how much you just saved my life on this @austenLacy

@TeodorKolev you need import this package in file:

import com.facebook.react.ReactActivity;
import com.facebook.react.ReactInstanceManager;

public class MainActivity extends ReactActivity {
   private ReactInstanceManager mReactInstanceManager;
   [...]
  
   @Override
    public void onBackPressed() {
      if (mReactInstanceManager != null) {
          mReactInstanceManager.onBackPressed();
      } else {
          super.onBackPressed();
      }
    }

I have included code, and I got

mReactInstanceManager has private access in ReactActivity
        if (mReactInstanceManager != null) {
            ^

I solved it, just add the polyfill for ES6 array in the entry file (mine is App.js):

require('core-js/es6/array')

export default App {
  // ...
}