Getting the Android hardware back button working

I’ve followed the detailed documentation, I’ve added the code to my app and when I run it with the Chrome debugger attached I can see the listener is called:
image
But… nothing is logged to the console and no navigation happens.

I’ve added the code to my main src/App.vue and it looks like

<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>

<script lang="ts">
import {
  IonApp,
  IonRouterOutlet,
  useBackButton,
  useIonRouter,
} from "@ionic/vue";
import { defineComponent, onMounted } from "vue";
import { useRouter } from "vue-router";
import { Plugins } from "@capacitor/core";
const { coreApp } = Plugins;

export default defineComponent({
  name: "App",
  components: {
    IonApp,
    IonRouterOutlet,
  },
  setup() {
    const ionRouter = useIonRouter();
    const router = useRouter();
    useBackButton(10, (processNextHandler) => {
      console.log("useBackButton handler was called!");
      if (ionRouter.canGoBack()) {
        console.log("Go back");
        router.back();
      }
      processNextHandler();
    });
    // I can combine these into one listener, but for testing - using 2
    useBackButton(-1, () => {
      if (!ionRouter.canGoBack()) {
        console.log("Exit app");
        coreApp.exitApp();
      }
    });
  },
});
</script>

What have I missed? I’m running @ionic/vue@5.8.4 and @ionic/vue-router@5.8.4 and the full dependencies section is below (I need to update everything)

{
  "dependencies": {
    "@capacitor-community/camera-preview": "file:../camera-preview",
    "@capacitor/android": "^3.2.0",
    "@capacitor/app": "1.0.2",
    "@capacitor/camera": "^1.0.3",
    "@capacitor/core": "^3.1.2",
    "@capacitor/haptics": "1.0.2",
    "@capacitor/ios": "3.2.0",
    "@capacitor/keyboard": "1.0.2",
    "@capacitor/network": "^1.0.2",
    "@capacitor/splash-screen": "^1.1.2",
    "@capacitor/status-bar": "1.0.2",
    "@ionic-native/app-version": "^5.36.0",
    "@ionic/pwa-elements": "^3.0.2",
    "@ionic/vue": "^5.8.4",
    "@ionic/vue-router": "^5.8.4",
    "@types/uuid": "^8.3.1",
    "animate.css": "^4.1.1",
    "class-transformer": "^0.4.0",
    "cordova-plugin-app-version": "^0.1.12",
    "core-js": "^3.6.5",
    "dayjs": "^1.10.6",
    "image-blob-reduce": "^3.0.1",
    "prettier": "^2.3.2",
    "reflect-metadata": "^0.1.13",
    "uuid": "^8.3.2",
    "vue": "^3.0.0-0",
    "vue-router": "^4.0.0-0"
  },
  "devDependencies": {
    "@capacitor/cli": "^3.1.2",
    "@types/jest": "^24.0.19",
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-e2e-cypress": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-unit-jest": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "@vue/test-utils": "^2.0.0-0",
    "capacitor-resources": "^2.0.5",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0",
    "typescript": "~3.9.3",
    "vue-jest": "^5.0.0-0"
  },
}

Can you send over a sample application that I can run on my machine?

@ldebeasi Sure thing. GitHub - pbowyer/ionic-android-hardware-back-button and instructions on the app’s Home screen.

The listener is at least called which is better than my “real” app.

I did first create a demo based on the sidemenu template and that closed the app on every hardware back button press, so I made this one instead.

There are a couple problems here:

  1. You do not need to do router.back() yourself. This functionality is built into Ionic. Your handler has a higher priority than Ionic’s built in navigation handler, so yours gets called first. Since you call processNextHandler, Ionic’s navigation handler gets called, and the app navigates again.
  2. There was a bug introduced in Ionic Vue 5.6.14 which results in ionRouter.canGoBack() not returning the correct result. I have filed an issue here and will look into a fix: bug: vue, ionRouter.canGoBack not working after 5.6.14 · Issue #24109 · ionic-team/ionic-framework · GitHub. You can work around this by staying on Ionic Vue 5.6.13 for now.

Ahh thank you. So you have to call useBackButton in order to enable this functionality, but once you do the navigating back is magically handled.

I see others have missed this too, as a GitHub usage search (which I did to see what I was doing wrong) has examples calling router.back() e.g. this one.

Which now makes sense given the navigating back is automatic… :+1:

Thanks! I will roll back to 5.6.13.

Ah, 5.6.13 of which package? @ionic/vue is on 5.8 so hoping it doesn’t have to be rolled back so far

Hardware Back Button functionality is enabled regardless of your usage of useBackButton, though you can disable it via a config option: Config | Ionic Documentation. The useBackButton feature is there to provide for more advanced usage of the back button that Ionic does not cover automatically.

The Hardware Back Button Docs go into this a bit more, but to summarize Ionic handles three things automatically:

  1. Closing overlays (modals, popovers, alerts, etc)
  2. Closing menus.
  3. Navigating backwards to a previous page.

Ah, 5.6.13 of which package? @ionic/vue is on 5.8 so hoping it doesn’t have to be rolled back so far

I recommend updating both @ionic/vue and @ionic/vue-router. I plan on taking a look at this issue this week though, so hopefully you should not have to roll back for too long.

I see why I’m confused: since I started this Ionic Vue app in June/July the hardware back button has never worked. I thought this was because I hadn’t called useBackButton.

It’s great to hear you plan to look at the issue this week; I’ll hold off rolling back in the expectation it’s soon fixed.

Ah interesting. Are you using Capacitor? With the Capacitor 3 migration, the @capacitor/app plugin needs to be installed for hardware back button to work.

Also I have a experimental build of the Ionic Vue fix for the hardware back button if you are interested in testing it out:

npm install @ionic/vue@5.9.0-dev.202110251709.f0e844d @ionic/vue-router@5.9.0-dev.202110251709.f0e844d

Thank you, that’s working for me :smiley:

1 Like

Thanks for testing! This should be fixed in an upcoming release of Ionic.

hello , my pages have been refreshed twice when i use capacitor hardware button to go to accueil.ts component , so my presence.ts component which should go to my accueil without second refresh

import {Component, OnInit, AfterViewInit, OnDestroy} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {StorageService} from '../services/storage/storage.service';
import {UserCrudService} from '../services/user/user-crud.service';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import {DataService} from "../services/DataService/data.service.ts.service";
import {Subscription} from "rxjs";
import {AutoUnsubscribe} from "../decorator/auto-unsuscribe.decorator";
import {AuthService} from "../services/AuthService/auth-service.service";
import {JourTravail} from "../entity/JourTravail/JourTravail";
import { App as CapacitorApp } from '@capacitor/app';
import {PluginListenerHandle} from "@capacitor/core";

@Component({
  selector: 'app-presence',
  templateUrl: './presence.page.html',
  styleUrls: ['./presence.page.scss'],
})
export class PresencePage implements OnInit,AfterViewInit ,OnDestroy {

  formPresence: FormGroup ;

  public  date: string;
  public username: string;
  private parameters: any ;
  private selectedDates :Date[];
  private token: string ;
  private parametres: string;
  private receivedParameters: any;
  private subscriptions: Subscription[] = [];
  private backButtonSubscription: Subscription;
  private getParam: Subscription;
  private backButtonListener: PluginListenerHandle=null;



  constructor(private route: ActivatedRoute, public formBuilder: FormBuilder,private myStorage: StorageService,private router: Router, private userService: UserCrudService, private dataService: DataService,  private authService: AuthService) {
    this.formPresence=  this.formBuilder.group({
      presM: false,
      presA: false,
      typeAbsence: null   // Ajout du nouveau contrôle de formulaire
    });
    //this.subscriptions.push(this.backButtonSubscription);
    this.backButtonListener = CapacitorApp.addListener('backButton', async({canGoBack}) => {
      if(!canGoBack){
        CapacitorApp.exitApp();
      } else {
        const param=JSON.stringify(this.parameters);
        console.log("les paramètres qui cont être envoyés à accueil sont "+param);
        await  this.router.navigate(['/accueil',{param}]);
      }
    });
  
  }



  ngOnInit() {
    registerLocaleData(localeFr);
    if (!this.authService.isLoggedIn()) {
      this.router.navigate(['login']);

    }
    // this.parameters= this.dataService.getData();
     // console.log(this.route.snapshot.params.param);
   try {
      this.parameters=JSON.parse(this.route.snapshot.params.param);
    }
  catch(error: any){
      console.log(error.message);
    }
    console.log(this.parameters);
      if (this.parameters) {
      console.log(this.parameters);
        this.username = this.parameters.username;
        this.selectedDates = this.parameters.selectedDates;
      }

    try {
      this.getParam = this.route.params.subscribe(params => {
        if (params['param']) {
          this.parameters = JSON.parse(params['param']);
          console.log(this.parameters.toString());
          this.username = this.parameters.username;
          this.selectedDates = this.parameters.selectedDates;
          console.log('Received Parameters:', this.parameters);
        }
      });
    } catch (error: any) {
      console.error(error.toString());
    }
    //this.subscriptions.push(this.backButtonSubscription);
    this.subscriptions.push(this.getParam);
   

}
  ngOnDestroy(): void {
    this.backButtonListener.remove;
    this.getParam.unsubscribe();
  }

ngAfterViewInit(){

}
  isPresenceMatin() {
    console.log(this.userService.getUsers());
  }

  isPresenceApresMidi() {
    console.log(this.userService.getUsers());
  }

  onSubmit() {
    this.username = this.parameters.username;
    let mesJoursDeTravail: JourTravail[] = [];
    const userSubscription = this.userService.getUserByUsername(this.username).subscribe(user => {
      this.selectedDates.forEach(uneDate => {
        console.log(uneDate);
      });
      const demiDaySubscription = this.userService.putDemiDay(mesJoursDeTravail).subscribe(value => {
        console.log(value);
      });

    });
   
    // this.router.navigate(['/'])
  }

  listeValidation() {
   //this.parameters=JSON.parse(this.route.snapshot.params.parameters);
   const parameters :any={
     username :this.username,

   }
    //this.username=this.parameters.username;
    //this.selectedDates=this.parameters.selectedDates;
    const param= JSON.stringify(parameters);
    this.router.navigate(['liste-validation',{param}])
  }

  returnHome() {
    if(localStorage.getItem('token')!=null) {
      this.router.navigate(['home']);
    }
  }

  noteDeFrais() {
    console.log('username:', this.username);
    console.log('mesDates:', this.selectedDates);

    if (this.username && this.selectedDates && this.selectedDates.length > 0) {
      let parameters:any = {
        username: this.username,
        selectedDates: Array.isArray(this.selectedDates) ? this.selectedDates : [this.selectedDates]
      }
      let param = JSON.stringify(parameters);
      console.log(param);
      this.router.navigate(['payer-note-de-frais', {param}]);
    } else {
      console.error('username or mesDates is not defined or mesDates is empty');
    }
  }
}

to

import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {ScreenOrientation} from "@ionic-native/screen-orientation/ngx";
import {NavController, Platform, ToastController} from "@ionic/angular";
import {FormGroup} from "@angular/forms";
import {DataService} from "../services/DataService/data.service.ts.service";
import {AuthService} from "../services/AuthService/auth-service.service";
import {Subscription} from "rxjs";
import {AutoUnsubscribe} from "../decorator/auto-unsuscribe.decorator";
import {App as CapacitorApp} from "@capacitor/app";
import {PluginListenerHandle} from "@capacitor/core";


@Component({
  selector: 'app-accueil',
  templateUrl: './accueil.page.html',
  styleUrls: ['./accueil.page.scss'],
})
export class AccueilPage implements OnInit ,AfterViewInit{
  formPresence: FormGroup;
  public date: string;
  public username: string;
  private parameters: any;
  private selectedDates: any[];
  private token: string;
  private parametres: string;
  private backButtonSubscription: Subscription ;
  private subscriptions : Subscription[] =[];
  private getParam: Subscription;
  private backButtonListener: PluginListenerHandle=null;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private screenOrientation: ScreenOrientation,
    private toastController: ToastController,
    private dataService: DataService,
    private authService: AuthService // Retirez le point-virgule
  ) {

    console.log("coucou on est  encore bien dans le constructeur")
    this.token = localStorage.getItem('token');
    this.backButtonListener = CapacitorApp.addListener('backButton', async({canGoBack}) => {
      if(!canGoBack){
        CapacitorApp.exitApp();
      } else {
        const param=JSON.stringify(this.parameters);
        console.log("les paramètres qui cont être envoyés à accueil sont "+param);
        await  this.router.navigate(['/accueil',{param}]);
      }
    });

  }

  ngAfterViewInit(): void {
    try {
      this.getParam= this.route.params.subscribe(params => {
        console.log("on rentre dans le bout de code qui permet de récupérer les données 13");
        if (params['param']) {
          try {
            this.parameters = JSON.parse(params['param']);
          }
          catch (error :any){
            console.error(error.toString());
          }
          try {
            this.selectedDates = this.parameters.selectedDates;
          }
          catch (error :any){
            console.error(error.toString())
          }
          console.log('Received Parameters:', this.parameters);
        }
      });
    }catch (error :any){
      console.error(error.toString());
    }
    }


  ngOnInit() {

    if (this.authService.isLoggedIn()) {
      this.token = localStorage.getItem('token');
      this.username = localStorage.getItem('username')
      /*try {
        console.log("les paramètres valent " + this.route.snapshot.params.param);
        this.parameters = JSON.parse(this.route.snapshot.params.param);
      } catch (error: any) {
        console.error(error.toString())
      }*/
     // console.log(this.parameters);

      /*try {
        this.username = this.parameters.username;
      } catch (error: any) {
        console.error(error.toString())
      }
      try {
        this.selectedDates = this.parameters.selectedDates;
      } catch (error: any) {
        console.error(error.toString())
      }
      console.log(this.selectedDates);*/
    } else {
      this.router.navigate(['login']);
    }



  }




  selectionner(){
    console.log(this.parameters);

    this.parameters={
      username:this.username,
      selectedDates:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    localStorage.setItem('data', param);

    try {
      if (localStorage.getItem('token') != null) {
        console.log("on est dans le if pour accéder à la route ");
        //this.dataService.setData(param);
        //console.log(this.dataService.getData());
        this.router.navigate(['/presence',{param}]);
      }
    }
    catch (error :any){
      console.log(error.toString());
    }
  }
  allerPageCompteur(){
    this.parameters={
      username:this.username,
      selectedDates : this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    if (localStorage.getItem('token') != null) {
      this.router.navigate(['/compteurs', {param}])
        .then(() => {
          // Navigation réussie
          // Vous pouvez ajouter du code ici si nécessaire
        })
        .catch((error) => {
          // Erreur lors de la navigation
          console.error(error.message);
          this.router.navigate(['/liste-validation',{param}]);
        });
    }
  }

  listeValidation() {

    this.parameters={
      username:this.username,
      date:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    if(localStorage.getItem('token') != null){
      this.router.navigate(['/avance-deplacement',{param}])
        .then(() => {
          // Navigation réussie
          // Vous pouvez ajouter du code ici si nécessaire
        })
        .catch((error) => {
          // Erreur lors de la navigation
          console.error(error.message);
          this.router.navigate(['/liste-validation',{param}]);
        });
    }
  }

  allerAvanceDeDeplacement() {
    this.parameters={
      username:this.username,
      date:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    if (this.selectedDates.length === 0 ) {
      this.showToastNombreDeDates();
      if(localStorage.getItem('token') != null){
        this.router.navigate(['/liste-validation',{param}])
      }
      //return; // Cela interrompt la fonction et ne continue pas vers la navigation.
    }
    if(localStorage.getItem('token') != null){
      this.router.navigate(['/avance-deplacement',{param}])
      }
  }

  allerPlanifierDeplacement() {
    console.log(this.selectedDates);
    this.parameters={
      username:this.username,
      date:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    if (this.selectedDates.length === 0 ) {
      this.showToastNombreDeDates();
      if(localStorage.getItem('token') != null){
        this.router.navigate(['/liste-validation',{param}])
      }
      //return; // Cela interrompt la fonction et ne continue pas vers la navigation.
    }
    if(localStorage.getItem('token') != null){
      this.router.navigate(['/avance-deplacement',{param}])
    }
  }
  ngOnDestroy() {
    // Désabonnement manuel de tous les abonnements
    try {
      this.getParam.unsubscribe();
    }
    catch (error : any)
    {
      console.error(error.toString());
    }
  }

  ordreDeplacement() {
    console.log(this.selectedDates);
    this.parameters={
      username:this.username,
      date:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    if (this.selectedDates.length === 0) {
      this.showToastNombreDeDates();
      if(localStorage.getItem('token') != null){
          this.router.navigate(['/liste-validation',{param}])
      }
      return; // Cela interrompt la fonction et ne continue pas vers la navigation.
    }
    this.router.navigate(['/ordre-de-deplacement',{param}])
      .then(() => {
        // Navigation réussie
        // Vous pouvez ajouter du code ici si nécessaire
      })
      .catch((error) => {
        // Erreur lors de la navigation
        console.error(error.message);
        this.router.navigate(['/liste-validation',{param}]);
        this.showToastNombreDeDates();
      });
  }


  async conges() {
    this.parameters={
      username:this.username,
      date:this.selectedDates
    };
    console.log(this.parameters);
    const param=JSON.stringify(this.parameters);
    console.log(param);
    try {
      await this.router.navigate(['/conges', {param}])
    } catch (error) {
      console.error("Erreur lors de la navigation :", error.message);
    }
  }
  async functionnalityToast() {
    const toast = await this.toastController.create({
      message: 'la fonctionnalité n\' est pas encore disponible',
      duration: 2000
    });
    await toast.present();
  }

  async showToastNombreDeDates() {
    const toast = await this.toastController.create({
      message: 'il n\'y a qu\'une date ',
      duration: 2000
    });
    await toast.present();
  }

  }

could you help me ?

video demonstration : WeTransfer - Send Large Files & Share Photos Online - Up to 2GB Free

So , it works with

window.history.back();

i find my parameters
Strangely

no, I’m wrong, no I’m still waiting for an answer

ok , i found the solution for capacitor !! :

constructor(private route: ActivatedRoute, public formBuilder: FormBuilder,private myStorage: StorageService,private router: Router, private userService: UserCrudService, private dataService: DataService,  private authService: AuthService) {
    this.formPresence=  this.formBuilder.group({
      presM: false,
      presA: false,
      typeAbsence: null   // Ajout du nouveau contrôle de formulaire
    });
    //this.subscriptions.push(this.backButtonSubscription);
    try {
      this.getParam = this.route.params.subscribe(params => {
        console.log("on rentre dans le bout de code qui permet de récupérer les données 13");
        if (params['param']) {
          try {
            this.parameters = JSON.parse(params['param']);
            console.log(this.parameters.username);
            this.username=this.parameters.username;
            // Vérifier si selectedDates existe et est un tableau
            if (Array.isArray(this.parameters.selectedDates)) {
              this.selectedDates = this.parameters.selectedDates.map(dateStr => {
                try {
                  // Convertir chaque chaîne de caractères en un objet Date
                  return new Date(dateStr);
                } catch (error) {
                  console.error("Erreur lors de la conversion de la date:", error.toString());
                  return null; // ou une date par défaut, si souhaité
                }
              }).filter(date => date !== null); // Filtrer les éventuelles valeurs null si la conversion échoue
            } else {
              console.error("selectedDates n'est pas un tableau ou est absent");
              this.selectedDates = []; // Assurer que selectedDates est toujours un tableau
            }

          } catch (error: any) {
            console.error("Erreur lors du parsing de 'param':", error.toString());
          }

          console.log('Received Parameters:', this.parameters);
        }
      });
    } catch (error: any) {
      console.error("Erreur lors de la souscription à route.params:", error.toString());
    }
    this.subscriptions.push(this.getParam);
  //  let parameters:any = {
    //  username: this.username,
      //selectedDates: this.selectedDates
   // }
   // let param = JSON.stringify(parameters);
    l//et backButtonFirstPress = true;

    // App.addListener('backButton', async({canGoBack}) => {
    //       this.router.navigate(['/accueil',{param}]);
    // });
    App.removeAllListeners();


  }

it seems there are aldready listener , and we have to close it .and then the problem is solved, no need to add another listener !, you have to add this line of code , in constructor : App.removeAllListeners(); or try to add this in ngOnDeestroy