Gestion du retour avec le router API

Bonjour à tous !

Je rencontre un souci avec ma gestion du retour avec le router api de Flutter.
En effet, la bouton « retour » de l’AppBar() native fonctionne parfaitement, mais ce n’est pas le cas du bouton natif d’android (en bas d’écran). Lorsque celui-ci est activé, l’application se ferme complétement …
Je retrouve le souci dans l’application de Purple Giraffe également.
Je n’ai rien trouvé de miraculeux sur internet alors si vous avez déjà rencontré le cas et trouvé une solution, je suis preneur !! :smiley:

Merci d’avance,
Bonne journée

Mathis

1 « J'aime »

C’est surement parce que tu fait un pushRemplacement() pour aller sur ta page. Dans ce cas tu supprimes ta page 1 pour la remplacer par ta page 2. Utilise un simple push() plutôt.

Edit : Si c’est pas le cas, montre nous comment tu fais qu’on puis mieux comprendre d’ou vient ton soucis :slight_smile:

Sinon une vidéo intéressante pour mettre en place un système de route (recommandé pour gérer ta navigation).

Merci @AntoLhn pour ta réponse.
Je n’utilise pas de pushRemplacement(), ni même de push() d’ailleurs. La vidéo que tu me conseilles ne semble donc pas correspondre à mon implémentation.
J’utilise le router api (navigation 2.0) avec la classe RouterDelegate.
Voici la classe en question :

class NavigationDelegate extends RouterDelegate<NavigationPath> with ChangeNotifier,
    PopNavigatorRouterDelegateMixin<NavigationPath>
    implements BottomNavBarRouter, HomeRouter, AppBarRouter
{

  bool _displayFavorites = false;
  Event? _eventDetails;
  User? _user;
  bool _displayProfil = false;


  @override
  Widget build(BuildContext context) {
    final List <Page<dynamic>> pagesList = [];
    final displayFavorites = _displayFavorites;
    final eventDetails = _eventDetails;
    final displayProfil = _displayProfil;
    final user = _user;
    final homeScreen = MyHomePage(this, HomeViewModel(this), this);
    pagesList.add(MaterialPage(child: homeScreen));
    if (displayProfil){
      if (user != null){
       //TODO : Page pour voir son profil
      }else{
        final loginScreen = LoginScreen(LoginViewModel(LoginService()));
        pagesList.add(MaterialPage(child: loginScreen));
      }
    }
    if (displayFavorites){
      final favoriteScreen = FavoriteScreen(this, HomeViewModel(this));
      pagesList.add(MaterialPage(child: favoriteScreen));
    }
    if (eventDetails != null){
      final eventDetail = EventDetailsScreen(eventDetails);
      pagesList.add(MaterialPage(child: eventDetail));
    }


    return Navigator(
      pages: pagesList,
      onPopPage: (route, result){
        if (route.didPop(result) == false){
          return false;
        }if(eventDetails != null){
          return onBackButtonTouchedDetailToPage();
        }else{
          return onBackButtonTouchedFavoriteToHome();
        }
      },
    );
  }

  bool onBackButtonTouchedDetailToPage(){
    if (_eventDetails != null){
      _eventDetails = null;
      notifyListeners();
    }
    return true;
  }

  bool onBackButtonTouchedFavoriteToHome(){
    if (_displayFavorites){
       _displayFavorites = false;
    }
    if (_eventDetails != null){
      _eventDetails = null;
    }if (_displayProfil){
      _displayProfil = false;
    }
    notifyListeners();
    return true;
  }

  @override
  NavigationPath? get currentConfiguration => NavigationPath(_displayFavorites, _eventDetails?.id);

  @override
  GlobalKey<NavigatorState>? navigatorKey = GlobalKey<NavigatorState>();

  @override
  Future<void> setNewRoutePath(NavigationPath configuration) async {
    final bool isDisplayFavoriteEnabled = configuration.displayFavorite;
    if (isDisplayFavoriteEnabled){
      _displayFavorites = true;
    }
  }

  @override
  displayFavorites() {
    _displayFavorites = true;
    notifyListeners();
  }

  @override
  displayHome() {
    _displayFavorites = false;
    notifyListeners();
  }

  @override
  DisplayDetail(Event event) {
    _eventDetails = event;
    notifyListeners();
  }

  @override
  displayProfil() {
    _displayProfil = true;
    notifyListeners();
  }
}

Merci d’avance,
Mathis

Ah oui ok, je vois l’idée. Je saisie ta méthode mais j’avoue ne pas avoir les connaissances nécessaires pour solutionner ton problème là…

Je passe mon tour :joy:

J’espère que quelqu’un sera t’aider.

Pas de souci ! Merci quand même et bonne soirée !

Salut @Mathis, tu as tout a fait raison j’ai bien le même problème sur l’app de Purple Giraffe :thinking:
Je vais essayer de comprendre d’où ça vient et je posterai ici la solution qui te servira aussi sur ton app

Salut @mbritto ,
Merci pour ton retour ! Je continue également de chercher de mon côté et posterais la solution, si je la trouve :smiley:
Bonne journée

1 « J'aime »

C’est bon j’ai trouvé !
En fait lorsque l’on crée le Navigator il faut l’associer à la clé que nous avons préparé au tout début, sinon lorsque l’évènement arrive, il ne retrouve pas le Navigator et ne peut pas appeler la fonction onPopPage.

Il te suffit d’ajouter la ligne : key: navigatorKey, dans le constructeur de ton Navigator en bas de la fonction build() ; comme ceci :

return Navigator(
      key: navigatorKey,
      pages: pagesList,
      onPopPage: (route, result) {
        if (route.didPop(result) == false) {
          return false;
        }
        return onBackButtonTouched(result);
      },
    );

C’était un joli bug, merci de l’avoir trouvé @Mathis !
Je vais mettre à jour l’app Android de Purple Giraffe et probablement mettre à jour le cours pour intégrer cette notion :+1:

3 « J'aime »

Super ! Merci pour la solution !
Bonne fin de journée

1 « J'aime »

Hello,
Je n’avais même pas remarqué que j’avais ce bug !!
C’est corrigé. Merci beaucoup !

Bonsoir, après avoir utilisé back_button_interceptor pour gérer ce petit problème je suis tombé sur ce thread et si une méthode existe dans passer par le back_button_interceptor je suis preneur.

J’ai donc rajouté la key dans le Navigator mais sans succès, la fonction de retour native Android quitte directement l’application (mais les boutons de retour dans l’Appbar fonctionnent correctement).

Après pas mal de recherche j’en ai compris que le bouton de retour Android est géré par un BackButtonDispatcher qui peut être passé en paramètre dans notre main :

return MaterialApp.router(
      key: navigatorKey,
      backButtonDispatcher: ,
...
);

Si ce paramètre n’est pas renseigné c’est un BackButtonDispatcher par défaut qui est généré.
Je ne suis pas certain d’avoir compris en détail le fonctionnement exact mais si j’ai bien compris au final la fonction popRoute du RouterDelegate est appelée à chaque événement concernant le bouton retour android.

Dans mon application j’ai créé une fonction bool _pop(){} qui me gère le retour je l’ai rajoutée directement dans la surcharge de la fonction popRoute :

@override
Widget build(BuildContext context) {
...
return Navigator(
      key: navigatorKey,
      pages: listePages,
      onPopPage: (route, result) {
        if (!route.didPop(result)) return false;
        return _onBackButtonTouched(result);
      },
    );
...
}

bool _onBackButtonTouched(dynamic result) => _pop();

@override
  Future<bool> popRoute() async {
    return _pop();
  }

bool _pop() {
    ...
    notifyListeners();
    return true;
}

Miracle ça fonctionne !
Je ne sais pas si c’est la bonne méthode mais je préfère ça à utiliser une libraire externe.

EDIT : Ma version de Flutter : 3.4.0-34.1.pre

Merci pour ton retour @isanforc !
C’est étrange que tu aies eu besoin de redéfinir popRoute(), normalement en fournissant la navigationKey ça devrait suffire.
A quoi ressemble ta déclaration de navigationKey ?
Voici celle de l’app de Purple Giraffe :

  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

Voici la ligne en question

@override
GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>();

Dans le fichier ça donne :

// Imports

class NavigationDelegate extends RouterDelegate<NavigationPath>
    with
        ChangeNotifier,
        PopNavigatorRouterDelegateMixin<NavigationPath>,
        //...
        {
  

  NavigationDelegate() {
    // ...
  }

  @override
  Widget build(BuildContext context) {
    final List<Page<dynamic>> listePages = [];

    // On ajoute la page principale
    if (!_serviceModel.enabled) homeTabPage = HomeTab.favorite;
    _homePageViewmodel.pageIndex = homeTabPage.index;
    listePages.add(
      MaterialPage(
        child: HomePage(_homePageViewmodel),
      ),
    );

    // ...

    return Navigator(
      key: navigatorKey,
      pages: listePages,
      onPopPage: (route, result) {
        if (!route.didPop(result)) return false;
        return _onBackButtonTouched(result);
      },
    );
  }

  bool _onBackButtonTouched(dynamic result) => _pop();

  bool _pop() {
    // ...
    notifyListeners();
    return true;
  }

  @override
  Future<bool> popRoute() async {
    return _pop();
  }

  // Page en cours en fonction des états (variables)
  @override
  NavigationPath? get currentConfiguration => NavigationPath(
        pageEnCours: pageEnCours,
        homeTabEnCours: homeTabPage,
      );

  // Obligatoire pour le fonctionnement du router
  @override
  GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>();

  // Gestion des écrans dans cette fonction à partir du NavigationPath (URL)
  @override
  Future<void> setNewRoutePath(NavigationPath configuration) async {
    if (configuration.pageEnCours != null) {
      pageEnCours = configuration.pageEnCours!;
    }

    if (configuration.homeTabEnCours != null) {
      homeTabPage = configuration.homeTabEnCours!;
    }
  }

  // ...
}

Ah ben voilà, je sais d’où vient ton problème :slight_smile:

Tu l’as mis sous la forme d’une propriété calculée, ce qu’il fait qu’il recrée une nouvelle clé à chaque fois que tu lis la propriété. Donc quand il la compare, ce sont des clés différentes et donc il n’appelle pas ta fonction onPopPage.

Crée une variable que tu remplies une seule fois et comme ça il gardera la même clé qu’il reconnaîtra au moment du back button de Android :

@override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

Je ne peux mettre la GlobalKey en final, j’ai une erreur de compilateur
image

J’ai testé également de passer par une variable, pas de bug de compilation mais aucune interception du bouton retour, l’application se ferme directement.

final GlobalKey<NavigatorState> _globalKey = GlobalKey<NavigatorState>();
// Obligatoire pour le fonctionnement du router
@override
GlobalKey<NavigatorState> get navigatorKey => _globalKey;

EDIT : Je viens de remarquer que tu n’avais pas mis de GET :sweat_smile: je reteste ça ce soir.

Aller, cette fois ci c’est la bonne, cette ligne fonctionne maintenant chez moi

@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

Par contre j’ai été obligé de garder mon appel à la méthode popRoute() pour intercepter le retour lorsque je suis sur la première page pour demander confirmation avant de quitter l’application.

@override
Future<bool> popRoute() async {
  return _pop();
}

Je ne sais pas si c’est du coup la bonne méthode, petite précision également je travaille sur Android uniquement

Je ne suis pas certain que ce soit un bon choix en terme d’ergonomie et d’expérience utilisateur, mais si c’est important pour toi alors je suppose que c’est une façon de le faire :slight_smile:

Hummmmmmm, j’aime trop ce forum

1 « J'aime »