How to exit application with twice clicking the back button without needing Redux
I was looking for a solution to limit the user and do not get out of the application with one click in react native.
I was looking for a solution to limit the user and do not get out of the application with one click in react native.
import React, {Component} from 'react';
import {BackHandler, View, Dimensions, Animated, TouchableOpacity, Text} from 'react-native';
let {width, height} = Dimensions.get('window');
export default class App extends Component<Props> {
state = {
backClickCount: 0
};
constructor(props) {
super(props);
this.springValue = new Animated.Value(100) ;
}
componentWillMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
_spring() {
this.setState({backClickCount: 1}, () => {
Animated.sequence([
Animated.spring(
this.springValue,
{
toValue: -.15 * height,
friction: 5,
duration: 300,
useNativeDriver: true,
}
),
Animated.timing(
this.springValue,
{
toValue: 100,
duration: 300,
useNativeDriver: true,
}
),
]).start(() => {
this.setState({backClickCount: 0});
});
});
}
handleBackButton = () => {
this.state.backClickCount == 1 ? BackHandler.exitApp() : this._spring();
return true;
};
render() {
return (
<View style={styles.container}>
<Text>
container box
</Text>
<Animated.View style={[styles.animatedView, {transform: [{translateY: this.springValue}]}]}>
<Text style={styles.exitTitleText}>press back again to exit the app</Text>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => BackHandler.exitApp()}
>
<Text style={styles.exitText}>Exit</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
}
}
const styles = {
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
},
animatedView: {
width,
backgroundColor: "#0a5386",
elevation: 2,
position: "absolute",
bottom: 0,
padding: 10,
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
},
exitTitleText: {
textAlign: "center",
color: "#ffffff",
marginRight: 10,
},
exitText: {
color: "#e5933a",
paddingHorizontal: 10,
paddingVertical: 3
}
};
Run in snack.expo: https://snack.expo.io/HyhD657d7
I've solved it this way as separated functional Component. This way you don't need to recode it for each app, only include the component in your new App and you've done!
import * as React from 'react';
import {useEffect, useState} from 'react';
import {Platform, BackHandler, ToastAndroid} from 'react-native';
export const ExecuteOnlyOnAndroid = (props) => {
const {message} = props;
const [exitApp, setExitApp] = useState(0);
const backAction = () => {
setTimeout(() => {
setExitApp(0);
}, 2000); // 2 seconds to tap second-time
if (exitApp === 0) {
setExitApp(exitApp + 1);
ToastAndroid.show(message, ToastAndroid.SHORT);
} else if (exitApp === 1) {
BackHandler.exitApp();
}
return true;
};
useEffect(() => {
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
backAction,
);
return () => backHandler.remove();
});
return <></>;
};
export default function DoubleTapToClose(props) {
const {message = 'tap back again to exit the App'} = props;
return Platform.OS !== 'ios' ? (
<ExecuteOnlyOnAndroid message={message} />
) : (
<></>
);
}
=> Only thing you need is to include this component in your App. <=
Because IOS don't have a back-button, IOS don't need this functionality. The above Component automatically detect if Device is Android or not.
By default the Message shown in Toast is predefined in English, but you can set your own one if you add an property named message
to your DoubleTapToClose-Component.
...
import DoubleTapToClose from '../lib/android_doubleTapToClose';
...
return(
<>
<DoubleTapToClose />
...other Stuff goes here
</>
Depending on your Nav-Structure, you have to check if you are at an initialScreen of your App or not. In my case, I have an Drawer with multiples StackNavigatiors inside. So I check if the current Screen is an initial-Screen (index:0), or not. If it is, I set an Hook-Variable which will only be used for those initial-Screens.
Looks like this:
const isCurrentScreenInitialOne = (state) => {
const route = state.routes[state.index];
if (route.state) {
// Dive into nested navigators
return isCurrentScreenInitialOne(route.state);
}
return state.index === 0;
};
...
...
export default function App() {
...
const [isInitialScreen, setIsInitialScreen] = useState(true);
{isInitialScreen && (<DoubleTapToClose message="Tap again to exit app" />)}
...
...
<NavigationContainer
...
onStateChange={(state) => {
setIsInitialScreen(isCurrentScreenInitialOne(state));
}}>
If that description helps you out, don't miss to vote.
canGoBack
just tell you whether you are about to exit the app? –
Jehovah canGoBack
Function in React-Navigation
. It seems you mixed-up reactJS
(and Browser-API... there you have an canGoBack) and react-native
/ react-navigation
. –
Genesisgenet The most simple way (paste directly in your functional component):
const navigation = useNavigation();
const navIndex = useNavigationState(s => s.index);
const [backPressCount, setBackPressCount] = useState(0);
const handleBackPress = useCallback(() => {
if (backPressCount === 0) {
setBackPressCount(prevCount => prevCount + 1);
setTimeout(() => setBackPressCount(0), 2000);
ToastAndroid.show('Press one more time to exit', ToastAndroid.SHORT);
} else if (backPressCount === 1) {
BackHandler.exitApp();
}
return true;
}, [backPressCount]);
useEffect(() => {
if (Platform.OS === 'android' && navIndex === 0) {
const backListener = BackHandler.addEventListener(
'hardwareBackPress',
handleBackPress,
);
return backListener.remove;
}
}, [handleBackPress]);
You can also wrap it in your custom hook and use the hook in the component(s).
Sorry if I'm late to the party, but I had a similar requirement and solved it by creating my own custom hook!
let currentCount = 0;
export const useDoubleBackPressExit = (exitHandler: () => void) => {
if (Platform.OS === "ios") return;
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
if (currentCount === 1) {
exitHandler();
subscription.remove();
return true;
}
backPressHandler();
return true;
});
};
const backPressHandler = () => {
if (currentCount < 1) {
currentCount += 1;
WToast.show({
data: "Press again to close!",
duration: WToast.duration.SHORT,
});
}
setTimeout(() => {
currentCount = 0;
}, 2000);
};
Now I can use it anywhere I want by simply doing:
useDoubleBackPressExit(() => {
// user has pressed "back" twice. Do whatever you want!
});
A Better Approach would be simply to use BackHandler and ToastAndroid
import { BackHandler, ToastAndroid } from 'react-native';
//rest of imports
class SomeClass extends Component{
constructor(state, props) {
super(state, props)
this.state = {
validCloseWindow: false
}
}
async componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
handleBackButton = () => {
if (!this.props.navigation.canGoBack()) {
if (this.state.validCloseWindow)
return false;
this.state.validCloseWindow = true
setTimeout(() => {
this.state.validCloseWindow = false
}, 3000);
ToastAndroid.show("Press Again To Exit !", ToastAndroid.SHORT);
return true;
}
};
//rest of component code
}
Just make sure to use it on the initialRoute page for your navigation.
Below Code Explains itself. The trick is to have it in the Main AppContainer rather in every page
import { Alert, BackHandler, ToastAndroid } from 'react-native';
import { StackActions } from 'react-navigation';
// common statless class variable.
let backHandlerClickCount = 0;
class App extends React.Component {
constructor(props) {
super(props);
// add listener to didFocus
this._didFocusSubscription = props.navigation.addListener('didFocus', payload =>
BackHandler.addEventListener('hardwareBackPress', () => this.onBackButtonPressAndroid(payload)));
}
// remove listener on unmount
componentWillUnmount() {
if (this._didFocusSubscription) {
this._didFocusSubscription.remove();
}
}
onBackButtonPressAndroid = () => {
const shortToast = message => {
ToastAndroid.showWithGravityAndOffset(
message,
ToastAndroid.SHORT,
ToastAndroid.BOTTOM,
25,
50
);
const {
clickedPosition
} = this.state;
backHandlerClickCount += 1;
if ((clickedPosition !== 1)) {
if ((backHandlerClickCount < 2)) {
shortToast('Press again to quit the application!');
} else {
BackHandler.exitApp();
}
}
// timeout for fade and exit
setTimeout(() => {
backHandlerClickCount = 0;
}, 2000);
if (((clickedPosition === 1) &&
(this.props.navigation.isFocused()))) {
Alert.alert(
'Exit Application',
'Do you want to quit application?', [{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel'
}, {
text: 'OK',
onPress: () => BackHandler.exitApp()
}], {
cancelable: false
}
);
} else {
this.props.navigation.dispatch(StackActions.pop({
n: 1
}));
}
return true;
}
}
Simplest One
import { useNavigationState } from '@react-navigation/native';
import { BackHandler, Alert } from 'react-native';
//index to get index of home screen when index == 0 in navigation stack
const index = useNavigationState(state => state.index);
const backAction = () => {
Alert.alert("Hold on!", "Are you sure you want to Exit App?", [
{
text: "Cancel",
onPress: () => null,
style: "cancel"
},
{ text: "YES", onPress: () => BackHandler.exitApp() }
]);
return true;
};
useEffect(() => {
// if index==0 this is initial screen 'Home Screen'
if (index == 0) {
BackHandler.addEventListener("hardwareBackPress", backAction);
return () =>
BackHandler.removeEventListener("hardwareBackPress", backAction);
}
}, [index]);
NavigationContainer
? Create a wrapper for each screen? –
Perianth import React, { Component } from 'react'
import { Text, View, StyleSheet, TouchableOpacity,BackHandler} from 'react-native'
import { Toast } from "native-base";
class Dashboard extends Component {
state={
clickcount:0
}
componentDidMount(){
BackHandler.addEventListener("hardwareBackPress",()=>{
this.setState({'clickcount':this.state.clickcount+1})
this.check();
return true
})
}
check=()=>{
if(this.state.clickcount<2){
Toast.show({
text:`Press back again to exit App `,
duration:2000,
onClose:()=>{this.setState({'clickcount':0})}
})
}
else if(this.state.clickcount==2)
{
BackHandler.exitApp()
}
}
render() {
return (
<View style={styles.container}>
<Text> Hello this is the Dashboard Screen</Text>
</View>
)
}
}
export default Dashboard
const styles = StyleSheet.create({
container:{
flex:1,
marginTop:25,
justifyContent:'center',
alignItems:'center',
borderBottomLeftRadius:30,
borderBottomRightRadius:30,
backgroundColor:'#fff'
},
});
the most simple approach for now:
in App.js:
componentDidMount() {
const backHandler=BackHandler.addEventListener('hardwareBackPress', ()=>{
if(this.backHandler){
return false;
}
Toast.show('再按一次退出应用');
this.backHandler=backHandler;
setTimeout(()=>{
this.backHandler=null;
},2000);
return true;
});
}
componentWillUnmount() {
this.backHandler.remove();
}
The simplest solution I used in my app is this. It works fine with react- navigation 4.4.1 and is much shorter than some other correct answers provided here.
import React from 'react';
import { BackHandler, ToastAndroid} from 'react-native';
export default class LoginScreen extends React.Component {
state = {
canBeClosed: false
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton);
}
handleBackButton = () => {
if (this.props.navigation.isFocused()) {
if (this.state.canBeClosed)
return this.state.canBeClosed = false;
else {
setTimeout(() => { this.state.canBeClosed = false }, 3000);
ToastAndroid.show("Press Again To Exit !", ToastAndroid.SHORT);
return this.state.canBeClosed = true
}
}
};
//some code
}
here is my solution, I don't used state, I have one main navigationContainer with nested navigator.
Inside main navigation:
imports...
const Stack = createNativeStackNavigator();
// this variable is never recreated
let exitCount = 0;
const Navigation = () => {
// action triggered on android back btn
const androidBackAction = () => {
// here is a little problem because I never clear the timeout, so you could have multiple timeout subscription at the same time.
// nonetheless after 2sec they clear by themselves, I think :)
setTimeout(() => {
exitCount = 0;
}, 2000);
if (exitCount === 0) {
ToastAndroid.showWithGravity(
"Click two times to close!",
ToastAndroid.SHORT,
ToastAndroid.BOTTOM
);
exitCount += 1;
} else if (exitCount === 1) {
BackHandler.exitApp();
}
return true;
};
const navigationChangeHandler = (state: NavigationState | undefined) => {
if (Platform.OS === "ios") return;
// check if current route is the initial one of the main navigation
// get the navigation
const currentNav = state?.routes[state.index];
// get inner route index
const nestedNavIndex = currentNav?.state?.index;
// back handler to exit the app
const androidBackExitHandler = BackHandler.addEventListener(
"hardwareBackPress",
androidBackAction
);
// remove exit app callback if inside not first route of nested nav
// => restore normal back behaviour
if (nestedNavIndex !== 0) {
androidBackExitHandler.remove();
}
};
return (
<NavigationContainer onStateChange={navigationChangeHandler}>
<Stack.Navigator
initialRouteName={NavConst.LOADING}
screenOptions={{
headerShown: false,
contentStyle: {
backgroundColor: Color.accent,
},
animation: "none",
}}
>
<Stack.Screen name={NavConst.LOADING} component={WaitingScreen} />
<Stack.Screen name={NavConst.TUTORIAL} component={Tutorial} />
<Stack.Screen name={NavConst.LOGIN_NAV} component={LoginNavigator} />
<Stack.Screen name={NavConst.MAIN_NAV} component={MainNavigator} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default Navigation;
I hope it could help!
For expo-router
, all of the above didn't directly work with me. I finally made my own below using "@Anton Liannoi" answer.
My requirement: It should work only on the first screen of app.
Use the hook in the root _layout.tsx
.
Hook code below 👇:
import { router } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { BackHandler, Platform, ToastAndroid } from "react-native";
export const useDoubleBackPressExit = () => {
const [backPressCount, setBackPressCount] = useState(0);
const handleBackPress = useCallback(() => {
if (!router.canGoBack()) {
if (backPressCount === 0) {
setBackPressCount(prevCount => prevCount + 1);
setTimeout(() => setBackPressCount(0), 2000);
ToastAndroid.show('Press again to exit', ToastAndroid.SHORT);
} else if (backPressCount === 1) {
BackHandler.exitApp();
}
} else {
router.back();
}
return true;
}, [backPressCount]);
useEffect(() => {
if (Platform.OS === 'android') {
const backListener = BackHandler.addEventListener(
'hardwareBackPress',
handleBackPress,
);
return backListener?.remove;
}
}, [handleBackPress])
}
© 2022 - 2024 — McMap. All rights reserved.