Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trouble handling android hardware back button #1819

Closed
Suresh-R-S opened this issue Jun 10, 2017 · 24 comments
Closed

Trouble handling android hardware back button #1819

Suresh-R-S opened this issue Jun 10, 2017 · 24 comments
Labels

Comments

@Suresh-R-S
Copy link

Hi,

I'am having trouble handling the android hardware back button.

If I use react-native BackHandler and add a listener in each of my screens, then I'am unable to remove the listener when the screen changes (I believe it is because I'am using StackNavigator & componentWillUnmount is not fired).

So, how to handle android hardware back button?

RN Version 0.44.0
react-navigation Version 1.0.0-beta.9
redux Version 3.6.0

@Selman555
Copy link

I've set up an app that seems to be working.
Try setting up a root component for your application and return the StackNavigator in your render method.

Handle all hardwareBackPress events inside this root component class.
You can (if you have a structure that allows it) manage all back presses from this location.

I am using redux and I use the BackHandlerevent to check if the app should exit or navigate back.

import React from 'react'
import {
    BackHandler,
} from 'react-native'
import {
    addNavigationHelpers,
    StackNavigator,
} from 'react-navigation'

export const AppNavigator = StackNavigator({ ... });

export class AppWithNavigationState extends React.Component {
    constructor(props) {
        super(props);
    }

    componentWillMount() {
        BackHandler.addEventListener('hardwareBackPress', function() {
            const { dispatch, navigation, nav } = this.props;
            if (nav.routes.length === 1 && (nav.routes[0].routeName === 'Login' || nav.routes[0].routeName === 'Start')) {
                return false;
            }
            // if (shouldCloseApp(nav)) return false
            dispatch({ type: 'Navigation/BACK' });
            return true;
        }.bind(this));
    }

    componentWillUnmount() {
        BackHandler.removeEventListener('hardwareBackPress');
    }

    render() {
        return <AppNavigator navigation={addNavigationHelpers({ dispatch: this.props.dispatch, state: this.props.nav })} />
    }
}

I haven't had any problems yet, but no one knows what the future will bring :)

@Suresh-R-S
Copy link
Author

Suresh-R-S commented Jun 15, 2017

Will try it out @Selman555 👍
Meanwhile, I handled the hardware back press by placing the listener inside onNavigationStateChange. And since I wanted to handle the back press for different screens in a custom manner, I emitted another event inside the hardwareBackPress. This event gets caught by the corresponding screen and the back press gets handled there. Not sure whether that is a good idea. But it seems to work. :)

@igofind
Copy link

igofind commented Jun 27, 2017

Try this,


import React, { PureComponent } from 'react';
import { Platform, BackHandler, ToastAndroid } from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import StackScenes from './scene/Scenes';

export default class extends PureComponent {

    constructor(props) {
        super(props);
        this.backButtonListener = null;
        this.currentRouteName = 'Main';
        this.lastBackButtonPress = null;
    }

    componentDidMount() {
        // do stuff while splash screen is shown
        // After having done stuff (such as async tasks) hide the splash screen
        SplashScreen.hide();

        if (Platform.OS === 'android') {
            this.backButtonListener = BackHandler.addEventListener('hardwareBackPress', () => {
                if (this.currentRouteName !== 'Main') {
                    return false;
                }

                if (this.lastBackButtonPress + 2000 >= new Date().getTime()) {
                    BackHandler.exitApp();
                    return true;
                }
                ToastAndroid.show('Something happened :)', ToastAndroid.SHORT);
                this.lastBackButtonPress = new Date().getTime();

                return true;
            });
        }
    }

    componentWillUnmount() {
        this.backButtonListener.remove();
    }

    render() {
        return (<StackScenes
            onNavigationStateChange={(prevState, currentState, action) => {
                this.currentRouteName = currentState.routes[currentState.index].routeName;
            }}
        />);
    }
}

@zivchen
Copy link

zivchen commented Aug 28, 2017

thanx @igofind

@Sean-Snow
Copy link

@Selman555 when use TabNavigator, This listener function will not execute。

structure

StackNavigator
    TabNavigator
        Home

in my Home page, BackHandler don't work

@agm1984
Copy link

agm1984 commented Oct 15, 2017

Selman555's code there worked beautiful for me. I haven't sampled any wild edge cases, but it definitely works great.

I recommend trying it and do not edit anything. I also haven't researched yet but 'Start' seems to have special meaning. It assists with stopping the app from closing if you press back when the stack only has one item and you're on the Login/Splash views, etc.

Here is my App.js file using it:

import React, { Component } from 'react'
import { Platform, BackHandler } from 'react-native'
import { Provider, connect } from 'react-redux'
import { addNavigationHelpers } from 'react-navigation'
import { NavigationStack } from './navigation/nav_reducer'
import store from './store'

class App extends Component {
    componentWillMount() {
        if (Platform.OS !== 'android') return
        BackHandler.addEventListener('hardwareBackPress', () => {
            const { dispatch, nav } = this.props
            if (nav.routes.length === 1 && (nav.routes[0].routeName === 'Login' || nav.routes[0].routeName === 'Start')) return false
            dispatch({ type: 'Navigation/BACK' })
            return true
        })
    }

    componentWillUnmount() {
        if (Platform.OS === 'android') BackHandler.removeEventListener('hardwareBackPress')
    }

    render() {
        const { dispatch, nav } = this.props
        const navigation = addNavigationHelpers({
            dispatch,
            state: nav
        })
        return <NavigationStack navigation={navigation} />
    }
}

const mapStateToProps = ({ nav }) => ({ nav })
const RootNavigationStack = connect(mapStateToProps)(App)

const Root = () => (
    <Provider store={store}>
        <RootNavigationStack />
    </Provider>
)

export default Root

@Selman555
Copy link

@Sean-Snow
Sorry, but that doesn't make sense. No matter where or in what context you push the back button, this event should be fired IF and only IF you set in up in the root component of your application.
In other words, a component that is ALWAYS loaded (never unloaded) in your application should host the event handler I mentioned.

Make sure that you haven't created a second back event handler somewhere else that cancels the root back event. I'm not 100% sure about this, but if you define another handler that returns false, your root event handler won't be handled as you might think.

@kelset
Copy link

kelset commented Nov 12, 2017

Pinging OP @Suresh-R-S since this issue has not been active for a while, and it's related to an old version of the lib (moreover solution provided by @Selman555 & @agm1984 seems to fulfill the issue) .

Please let me know if you want this to remain open, because if I get no answer in the next 7 days I will close it.

@kelset
Copy link

kelset commented Nov 19, 2017

Hi there @Suresh-R-S ,

In an effort to cleanup this project and prioritize a bit, since this issue had no follow up since my last comment I'm going to close it.

If you are still having the issue with the latest version (especially if it's a bug report) please check the current open issues, and if you can't find a duplicate, open a new one that uses the new Issue Template to provide some more details to help us solve it.

@kelset kelset closed this as completed Nov 19, 2017
@salujaharkirat
Copy link

@Suresh-R-S even I wanted to handle the back press for different screens in a custom manner. Can you help me out here please?

@KGSachinthaU
Copy link

KGSachinthaU commented Feb 16, 2018

@Selman555 code with me to come cannot read property index of undefined -> related to createNavigationContainer


import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View,
  AsyncStorage,
  ActivityIndicator,
  Alert,
  BackHandler,
} from 'react-native';
import { BASE_URL } from './constants'
import LoginScreen from './screens/loginScreen'
import MainScreen from './screens/mainScreen'
import NavigationDrawer from './components/navigationDrawer'
import LoginNavigation from './config/router'
import DeviceDetailsScreen from './screens/deviceDetailsScreen';
import Test from './screens/deviceDetailsScreen'
import { addNavigationHelpers } from 'react-navigation'


export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      isLoggedIn: false,
      isLaunching: true,
    }
    this.onLogOutPress = this.onLogOutPress.bind(this)
    this.onLoginPress = this.onLoginPress.bind(this)
  }

  componentWillMount() {
    BackHandler.addEventListener('hardwareBackPress', function () {
      const { dispatch, navigation, nav } = this.props;
      if (nav.routes.length === 1 && (nav.routes[0].routeName === 'Login' || nav.routes[0].routeName === 'Start')) {
        return false;
      }
      dispatch({ type: 'Navigation/BACK' });
      return true;
    }.bind(this));
  }

  componentWillUnmount() {
    BackHandler.removeEventListener('hardwareBackPress');
  }

  async componentDidMount() {
    let email = await AsyncStorage.getItem('@Email:key');
    let password = await AsyncStorage.getItem('@Password:key');
    let token = await AsyncStorage.getItem('@Token:key');

    if (email && password) {

      AsyncStorage.getItem('@Token:key').then((token) => {

        return fetch(BASE_URL + '/buildings', {
          method: 'GET',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + token
          },
        }).then((responseOne) => {
          if (responseOne.status == 200) {
            this.setState({ isLoggedIn: true, isLaunching: false })
          } else {
            this.setState({ isLoggedIn: false, isLaunching: false })
          }
        })
      })
    } else {
      this.setState({ isLoggedIn: false, isLaunching: false })
    }
  }

  async onEditPress() {
    Alert.alert(
      'Alert Title',
      await AsyncStorage.getItem('@Token:key'),
      [
        { text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
        { text: 'OK', onPress: () => console.log('OK Pressed') },
      ],
      { cancelable: false }
    )
  }

  async onLogOutPress() {
    await AsyncStorage.removeItem('@Token:key');
    await AsyncStorage.removeItem('@UserType:key');
    this.setState({ isLoggedIn: false })
  }

  onLoginPress() {
    this.setState({ isLoggedIn: true })
  }

  render() {
    if (this.state.isLaunching) {
      {
        return (
          <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'transparent' }}>
            <ActivityIndicator size="large" />
          </View>
        );
      }
    } else if (this.state.isLoggedIn)
      return <NavigationDrawer onLogOutPress={this.onLogOutPress} />
    else {

      return <LoginNavigation navigation={addNavigationHelpers({ dispatch: this.props.dispatch, state: this.props.nav })} />
    }
  }
}
//navigation={addNavigationHelpers({ dispatch: this.props.dispatch, state: this.props.nav })}

@brentvatne
Copy link
Member

this may resolve issues that folks encountered here: b3bf806

if not, please create a new issue with an example on snack.expo.io or github, thanks!

@TRIPTI-JAIN
Copy link

Here is the code
import { BackHandler,
Alert
} from 'react-native';

constructor(props) {
super(props);
this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
}

componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButtonClick);
}

  handleBackButtonClick() {
    Alert.alert(
       'Exit App',
       'Do you want to exit?',
       [
         {text: 'No', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
         {text: 'Yes', onPress: () => BackHandler.exitApp()},
       ],
       { cancelable: false })

  return true;
  }

This code works. I used it

@basitsattar
Copy link

basitsattar commented Apr 8, 2018

In my case I was using a stack navigator so I had to get the stack first and then check its routes.

BackHandler.addEventListener('hardwareBackPress', () => { const { dispatch, nav } = this.props const stack = nav.routes[0]; if (stack.routes.length === 1) { Alert.alert( 'Exit App', 'Do you want to exit?', [ { text: 'No', onPress: () => { } }, { text: 'Yes', onPress: () => BackHandler.exitApp() }, ], { cancelable: false }) } dispatch({ type: 'Navigation/BACK' }) return true })

@helloo0-0oword
Copy link

How to use it in react navigation v2 :(

@jamesone
Copy link

Is there a way we can configure this for rect-navigation v2 & without redux integration?

@lavarajallu
Copy link

lavarajallu commented Nov 13, 2018

How to use in react-native-router-flux

@Johncy1997
Copy link

Suppose i am on initial screen on stackNavigator, When i press back it shows Alert message and not waits for user to press ok or cancel..Anyone tell me what should i do to pause exit function.

@dianazzz
Copy link

dianazzz commented May 7, 2019

Suppose i am on initial screen on stackNavigator, When i press back it shows Alert message and not waits for user to press ok or cancel..Anyone tell me what should i do to pause exit function.

Show the alert popup in a timeout and add return true; after that.

goBack = ()=>{
   setTimeOut(()=>{
      Alert.show()
   },10);
 return true;
}

@dianazzz
Copy link

dianazzz commented May 7, 2019

I have a stack navigator. Inside the stack navigator I have drawerNavigator. I added the backhandler listener in App.js. It get called in Stacknavigator. But not in inner pages of DrawerNavigator. I would like to make backHandler a common function. How can I acheive this?

export const RootStack = createStackNavigator(
  {
    Login: {
      screen: LoginScreen
    },   
    Register: {
      screen: RegisterScreen
    },    
    Home: {
      screen: MyDrawer
    }
  },
  {
    headerMode: "none",
    initialRouteName: "Login",
    navigationOptions: {
      gesturesEnabled: false,
      drawerLockMode: "locked-closed"
    }
  }
);

@kaytee319
Copy link

I've resolved this by using this.props.navigation.isFocused() function

Example:

componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}

handleBackPress = () => {
if(this.props.navigation.isFocused()) {
this.props.navigation.goBack()
}
return true;
}

@kkorezelidis
Copy link

please check this hook: https://github.com/react-navigation/hooks#usefocuseffectcallback

useFocusEffect can be helpful to handle hardware back behavior on currently focused screen:

@shashwatvarshney
Copy link

can anyone tell me how to use software back press key in react native in back handling button?

@Selman555
Copy link

Selman555 commented Apr 14, 2020

can anyone tell me how to use software back press key in react native in back handling button?

Do you mean Android's onscreen back button, IOS's swipe gesture?
In that case BackHandler.addEventListener('hardwareBackPress', () => boolean should do the trick.

If you're talking about a custom made back button, the button's onPress={() => this.props.navigation.goBack()} should be what you're looking for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests