html – Android Back Button on a Progressive Web Application closes de App-ThrowExceptions

Exception or error:

Can a “pure” HTML5/Javascript (progressive) web application intercept the mobile device back button in order to avoid the App to exit?

This question is similar to this one but I want to know if it is possible to achieve such behavior without depending on PhoneGap/Ionic or Cordova.

How to solve:

While the android back button cannot be directly hooked into from within a progressive web app context, there exists a history api which we can use to achieve your desired result.

First up, when there’s no browser history for the page that the user is on, pressing the back button immediately closes the app.
We can prevent this by adding a previous history state when the app is first opens:

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

The documentation for this function can be found on mdn:

pushState() takes three parameters: a state object, a title (which is currently ignored), and (optionally) a URL[…] if it isn’t specified, it’s set to the document’s current URL.

So now the user has to press the back button twice. One press brings us back to the original history state, the next press closes the app.


Part two is we hook into the window’s popstate event which is fired whenever the browser navigates backwards or forwards in history via a user action (so not when we call history.pushState).

A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document.

So now we have:

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})

When the page is loaded, we immediately create a new history entry, and each time the user pressed ‘back’ to go to the first entry, we add the new entry back again!


Of course this solution is only so simple for single-page apps with no routing. It will have to be adapted for applications that already use the history api to keep the current url in sync with where the user navigates.

To do this, we will add an identifier to the history’s state object. This will allow us to take advantage of the following aspect of the popstate event:

If the activated history entry was created by a call to history.pushState(), […] the popstate event’s state property contains a copy of the history entry’s state object.

So now during our popstate handler we can distinguish between the history entry we are using to prevent the back-button-closes-app behaviour versus history entries used for routing within the app, and only re-push our preventative history entry when it specifically has been popped:

window.addEventListener('load', function() {
  window.history.pushState({ noBackExitsApp: true }, '')
})

window.addEventListener('popstate', function(event) {
  if (event.state && event.state.noBackExitsApp) {
    window.history.pushState({ noBackExitsApp: true }, '')
  }
})

The final observed behaviour is that when the back button is pressed, we either go back in the history of our progressive web app’s router, or we remain on the first page seen when the app was opened.

###

@alecdwm, that is pure genius!

Not only does it work on Android (in Chrome and the Samsung browser), it also works in desktop web browsers. I tested it on Chrome, Firefox and Edge on Windows, and it’s likely the results would be the same on Mac. I didn’t test IE because eew. Even if you’re mostly designing for iOS devices that have no back button, it’s still a good idea to ensure that Android (and Windows Mobile… awww… poor Windows Mobile) back buttons are handled so that the PWA feels much more like a native app.

Attaching an event listener to the load event didn’t work for me, so I just cheated and added it to an existing window.onload init function I already had anyhow.

Keep in mind that it might frustrate users who would actually want to really Go Back to whatever web page they were looking at before navigating to your PWA while browsing it as a standard web page. In that case, you can add a counter and if the user hits back twice, you can actually allow the “normal” back event to happen (or allow the app to close).

Chrome on Android also (for some reason) added an extra empty history state, so it took one additional Back to actually go back. If anyone has any insight on that, I’d be curious to know the reason.

Here’s my anti-frustration code:

var backPresses = 0;
var isAndroid = navigator.userAgent.toLowerCase().indexOf("android") > -1;
var maxBackPresses = 2;
function handleBackButton(init) {
    if (init !== true)
        backPresses++;
    if ((!isAndroid && backPresses >= maxBackPresses) ||
    (isAndroid && backPresses >= maxBackPresses - 1)) {
        window.history.back();
    else
        window.history.pushState({}, '');
}
function setupWindowHistoryTricks() {
    handleBackButton(true);
    window.addEventListener('popstate', handleBackButton);
}

###

This approach has a couple of improvements over existing answers:

Allows the user to exit if they press back twice within 2 seconds: The best duration is debatable but the idea of allowing an override option is common in Android apps so it’s often the correct approach.

Only enables this behaviour when in standalone (PWA) mode: This ensures the website keeps behaving as the user would expect when within an Android web browser and only applies this workaround when the user sees the website presented as a “real app”.

function isStandalone () {
    return !!navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
}

// Depends on bowser but wouldn't be hard to use a
// different approach to identifying that we're running on Android
function exitsOnBack () {
    return isStandalone() && browserInfo.os.name === 'Android';
}

// Everything below has to run at page start, probably onLoad

if (exitsOnBack()) handleBackEvents();

function handleBackEvents() {
    window.history.pushState({}, '');

    window.addEventListener('popstate', () => {
        //TODO: Optionally show a "Press back again to exit" tooltip
        setTimeout(() => {
            window.history.pushState({}, '');
            //TODO: Optionally hide tooltip
        }, 2000);
    });
}

###

i did not want to use native javascript functions to handle this inside of a react app, so i scoured solutions
that used react-router or react-dom-router, but in the end, up against a deadline, native js is
what got it working. Added the following listeners inside inside componentDidMount() and setting the history
to an empty state

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})

this worked fine on the browser, but was still not working in the PWA on mobile
finally a colleague found out that triggering the history actions via code is what somehow initiated the listeners
and voila! everything fell in place

window.history.pushState(null, null, window.location.href); 
window.history.back(); 
window.history.forward(); 

Leave a Reply

Your email address will not be published. Required fields are marked *