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

Created on 4 Oct 2015  路  20Comments  路  Source: facebook/react-native

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.

Locked

Most helpful comment

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 /> }
}

All 20 comments

I had to add the following to MainActivity.java,

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

@satya164 your code works!

@satya164 Thank's a lot, it works.
Maybe it should be added to the docs ?

@vincelwt It was absent from the boilerplate app previously. I believe it has been added.

Yes, the back button handler will be added to all new apps by react-native init by default in the 0.12 release: https://github.com/facebook/react-native/blob/master/local-cli/generator-android/templates/package/MainActivity.java

Closing this on as @satya164 provided the correct solution for existing apps.

Thanks for reporting!

Hi

I have included the code snippet in MainActivity. But Here is my observation:

When the app loads and the first scene is rendered, here when i press the hardware back button, the app quits because I am returning false from my implementation of BackAndroid.

But when I move to the second scene and again when I come back to the first scene and then press the hardware back button, the app does not quit.

Is this the default expected behaviour? My implementation is as follows:
BackAndroid.addEventListener('hardwareBackPress', () => {return false;});

Another Observation is:
When the app loads the third scene and when back button is pressed there, Instead of just going back one scene, It goes back to the first scene.
The implementation is as follows:
BackAndroid.addEventListener('hardwareBackPress', () => {if (this.props.nav) {this.props.nav.pop(); return true;} return false;});

@satya164 @mkonicek Any comments?

Thanks in Advance
Lalith

I have included code, and I got

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

@TeodorKolev So do I, any solution?

I have a question here. If I add these code in MainActivity:

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

Did I need to add BackAndroid Listener in JS? Maybe just leave one of them? Any suggestions?

mReactInstanceManager is private. So I can not add this to MainActivity

@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();
      }
    }

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 cannot overstate how much you just saved my life on this @austenLacy

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

@austenLacy Your solution worked for me. Thank you.

@austenLacy, you da bomb!

still a problem, React Native team please fix this ASAP, I'm using expo so won't be able to modify android folder :(

I upgraded my RN version to 0.55.4 from 0.52 , initially backhandler was working perfectly but after upgrade it stopped listening.

Hi, I found this is a bug of Set in react-native. Look at this code:

var set = new Set()
set.add('a')
console.warn(set.has('a'))
console.warn(Array.from(set.values())) 

In node interpreter the output is true ['a'], but in react-native that is true [].

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 {
  // ...
}
Was this page helpful?
0 / 5 - 0 ratings