Réflexion sur le Navigator 2

Bonjour à tous,

J’aimerai ouvrir une petite réflexion sur le Navigator 2, je suis très hypé par ce routeur, je l’ai intégré dans qqs application internes mais je reste sur ma faim concernant quelques points. Peut être que je ne l’utilise pas correctement ou que je n’ai pas encore découvert toutes ses possibilité (ce qui est certainement le cas), voici ce qui me dérange un peu :

Le NavigationDelegate saturé

Lorsque l’on a une application utilisant beaucoup d’écrans dans notre application le NavigationDelegate est très vite saturé de petites fonctions permettant l’ouvertures de nouvelles pages.

Exemple :

  @override
  void openAddServicePage() {
    pageEnCours = PageType.service;
    serviceSubPage = ServiceSubPage.newService;
    addServicePageViewmodel.reset();
    notifyListeners();
  }

  @override
  void openDeleteCurrentServicePage() {
    pageEnCours = PageType.service;
    serviceSubPage = ServiceSubPage.deleteService;
    notifyListeners();
  }

  @override
  void openSelectServicePage() {
    pageEnCours = PageType.service;
    serviceSubPage = ServiceSubPage.selectService;
    notifyListeners();
  }

  @override
  void openSettingsPage() {
    pageEnCours = PageType.parametres;
    settingSubPage = null;
    notifyListeners();
  }

  @override
  void openSettingsRegionSelector() {
    pageEnCours = PageType.parametres;
    settingSubPage = SettingSubPage.regionSelector;
    notifyListeners();
  }

etc...

Je pourrais peut être utiliser une fonction avec des paramètres, mais j’aime bien l’idée de pouvoir séparer les fonctions cela me permet ensuite de découper mes PagesRouter en fonction des différentes sections de mon application

Exemple :

abstract class ServiceRouter {
  void openAddServicePage();
  void openSelectServicePage();

  void showServiceTab({int idService = 0});
  void reloadApp();
  void openDeleteCurrentServicePage();
}
abstract class SettingsRouter {
  void openSettingsPage();
  void openSettingsRegionSelector();
  void openSettingsDepartementSelector();
  void openSettingsDatabaseUpdater();
  void reloadSearchController();
  void reloadApp();

  void openSettingsContactListPage();
  void openSettingsContactCreatePage(String? idBrigade);
  void openSettingsContactViewPage(int idContact);
  void openSettingsContactEditPage(int idContact);
}

Ainsi dans mes Viewmodel j’appelle le router qui me convient, la programmation est plus propre (avis très subjectif :slight_smile: )

Qu’en pensez vous ? Avez vous des idées ?

Une piste me vient à l’esprit et c’est une transition toute trouvée pour la suite.

Utiliser plusieurs Navigator ?

Le titre parle de lui même, est il possible d’utiliser plusieurs Navigator dans la même application ?

Prenons l’exemple d’un site web, j’ai plusieurs sections dans mon site :

  • Blog
  • Contact
  • Page d’erreur
  • Forum

Si les sections Contact et page d’erreur ne seraient composées que d’une seule page, les sections Blog et Forum le seront de beaucoup plus.

Donc l’idée serait d’avoir un Navigator principal qui gère un premier niveau l’accès aux sections.
Ensuite dans les sections on pourrait avoir un second Navigator qui gère les pages, et une page pourrait contenir elle même un Navigator s’il y a plusieurs sous page etc…

Cela donnerait par exemple :

Cela est il possible ?

Il faudrait garder la gestion des URL, comment alors gérer nos NavigationRouteParser pour que cela fonctionne ?

J’aime bien l’idée de pouvoir séparer en sections, aussi les sections devraient pouvoir être autonomes mais il devrait être possible d’ouvrir la page contact depuis par exemple un article du blog.

Conclusion

Je pense que le Navigator est un très bon outil, et je rejoint @mbritto sur l’envie d’utiliser le moins possible de librairies externes c’est pour cette raison que je n’ai pas cherché bonheur sur pub.dev.

J’ai la conviction qu’il est possible de réaliser cela avec Flutter et aujourd’hui je suis à la recherche du « comment réaliser cela ? ».

Aussi le sujet est lancé et je suis extrêmement curieux de connaitre votre expérience et vos idées !

Je continue dans ma lancée, j’ai trouvé une alternative avec des Singleton, cependant je ne connais pas l’impact de l’utilisation de Singleton en terme de performance.

Désolé pour l’absence de commentaires, j’ai codé ça en live ce matin je n’ai pas pris le temps de documenter tout ça.

Je n’ai pas mis en place la gestion par Viewmodel mais cette variante est à mon sens compatible.

NavigationPath

En réalité dans mon utilisation je n’en ai pas besoin, est il possible de le supprimer ?

class NavigationPath {
  NavigationPath();
}

NavigationRouteParser

Dans mon utilisation je n’ai pas besoin de revenir sur cette classe, elle restera telle que.

import 'package:flutter/material.dart';
import 'package:fracosfr/router/modules/main.dart';
import 'package:fracosfr/router/navigation_path.dart';

class NavigationRouteParser extends RouteInformationParser<NavigationPath> {
  @override
  Future<NavigationPath> parseRouteInformation(
      RouteInformation routeInformation) {
    MainRouteConfiguration().parseRoute(routeInformation);
    return Future.value(NavigationPath());
  }

  @override
  RouteInformation? restoreRouteInformation(NavigationPath configuration) {
    return MainRouteConfiguration().restoreRoute(configuration) ??
        const RouteInformation(location: "/");
  }
}

NavigationDelegate

Comme pour le NavigationRouteParser je ne touche plus au NavigationDelegate, la logique est déplacée dans la classe MainRouteConfiguration

import 'package:flutter/material.dart';
import 'package:fracosfr/router/modules/main.dart';
import 'package:fracosfr/router/navigation_path.dart';

class NavigationDelegate extends RouterDelegate<NavigationPath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<NavigationPath> {
  MainRouteConfiguration mainRouteConfiguration = MainRouteConfiguration();
  bool directusLoaded = false;

  NavigationDelegate() {
    mainRouteConfiguration.addListener(() {
      notifyListeners();
    });
  }

  @override
  NavigationPath? get currentConfiguration => NavigationPath();

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

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

    pagesList.addAll(MainRouteConfiguration().getPages());

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

  bool onBackButtonTouched(dynamic result) {
    return MainRouteConfiguration().onBackButtonTouched(result);
  }

  @override
  Future<void> setNewRoutePath(NavigationPath configuration) async {}
}

MainRouteConfiguration

La classe qui est la plus importante, on retrouve dedans des fonctions présentes dans le NavigationDelegate, cette classe sera mon « routeur » de niveau 1.

Cette classe est utilisée en Singleton.

import 'package:flutter/material.dart';
import 'package:fracosfr/router/modules/blog.dart';
import 'package:fracosfr/router/navigation_path.dart';
import 'package:fracosfr/ui/page_test.dart';

enum ModulePage {
  home,
  blog,
  contact,
  error,
}

class MainRouteConfiguration extends ChangeNotifier {
  static MainRouteConfiguration? _instance;

  factory MainRouteConfiguration() {
    _instance ??= MainRouteConfiguration._internal();
    return _instance!;
  }

  /// Internal constructor
  MainRouteConfiguration._internal() {
    BlogRouteConfiguration().addListener(() {
      notifyListeners();
    });
  }

  ModulePage _module = ModulePage.home;

  void parseRoute(RouteInformation routeInformation) {
    final uri = Uri.parse(routeInformation.location ?? "/");
    _module = ModulePage.home;

    if (uri.pathSegments.isNotEmpty) {
      _module = ModulePage.error;
      switch (uri.pathSegments.first) {
        case "contact":
          _module = ModulePage.contact;
          break;
        case "blog":
          BlogRouteConfiguration()
              .parseRoute(uri.pathSegments.sublist(1), uri.query);
          _module = ModulePage.blog;
          break;
      }
    }
  }

  RouteInformation? restoreRoute(NavigationPath configuration) {
    if (_module == ModulePage.error) return null;
    if (_module == ModulePage.home) {
      return const RouteInformation(location: "/");
    }
    if (_module == ModulePage.blog) {
      return RouteInformation(
          location: "/blog${BlogRouteConfiguration().restoreRoute()}");
    }

    return RouteInformation(
        location: "/${MainRouteConfiguration()._module.name}");
  }

  bool onBackButtonTouched(dynamic result) {
    if (_module == ModulePage.blog) {
      if (BlogRouteConfiguration().onBackButtonTouched(result)) return true;
    }
    _module = ModulePage.home;
    notifyListeners();
    return true;
  }

  List<Page> getPages() {
    List<Page> pages = [];

    pages.add(const MaterialPage(child: PageTest("accueil")));

    if (_module == ModulePage.blog) {
      pages.addAll(BlogRouteConfiguration().getPages());
    }

    if (_module == ModulePage.contact) {
      pages.add(const MaterialPage(child: PageTest("contact")));
    }

    if (_module == ModulePage.error) {
      pages.add(const MaterialPage(child: PageTest("error")));
    }

    return pages;
  }

  void changeModule(ModulePage modulePage) {
    _module = modulePage;
    notifyListeners();
  }
}

MainRouter

Le MainRouter était dans l’exemple du cours une classe abstraite, je l’ai reconvertie en classe normale avec des fonctions statiques, ce qui me permet dans toute mon application d’appeler des fonctions comme MainRouter.openBlogPage(); pour changer de page.

import 'package:fracosfr/router/modules/main.dart';

class MainRouter {
  static void openContactPage() {
    MainRouteConfiguration().changeModule(ModulePage.contact);
  }

  static void openHomePage() {
    MainRouteConfiguration().changeModule(ModulePage.home);
  }

  static void openBlogPage() {
    MainRouteConfiguration().changeModule(ModulePage.blog);
  }

  static void openErrorPage() {
    MainRouteConfiguration().changeModule(ModulePage.error);
  }
}

BlogRouteConfiguration

Un sous routeur pour la gestion d’un blog.

Cette classe est utilisée en Singleton.

import 'package:flutter/material.dart';
import 'package:fracosfr/router/modules/main.dart';
import 'package:fracosfr/ui/blog_article.dart';
import 'package:fracosfr/ui/blog_list.dart';

enum ModuleBlogPage {
  liste,
  article,
}

class BlogRouteConfiguration extends ChangeNotifier {
  static BlogRouteConfiguration? _instance;

  factory BlogRouteConfiguration() {
    _instance ??= BlogRouteConfiguration._internal();
    return _instance!;
  }

  BlogRouteConfiguration._internal();

  ModuleBlogPage _blogPage = ModuleBlogPage.liste;
  String? _idArticle;

  void changeModule(ModuleBlogPage moduleBlogPage, {String? id}) {
    MainRouteConfiguration().changeModule(ModulePage.blog);
    _blogPage = moduleBlogPage;
    _idArticle = id;
    notifyListeners();
  }

  void parseRoute(List<String> segments, String? query) {
    _blogPage = ModuleBlogPage.liste;
    _idArticle = null;
    if (segments.length >= 2) {
      if (segments.first == "article") {
        _blogPage = ModuleBlogPage.article;
        _idArticle = segments[1];
      }
    }
  }

  String restoreRoute() {
    if (_blogPage == ModuleBlogPage.article && _idArticle != null) {
      return "/article/$_idArticle";
    }
    return "";
  }

  bool onBackButtonTouched(dynamic result) {
    if (_blogPage == ModuleBlogPage.liste) return false;
    _blogPage = ModuleBlogPage.liste;
    notifyListeners();
    return true;
  }

  List<Page> getPages() {
    List<Page> pages = [];

    pages.add(const MaterialPage(child: BlogListPage()));

    if (_blogPage == ModuleBlogPage.article) {
      pages.add(MaterialPage(child: BlogArticlePage(_idArticle ?? "")));
    }

    return pages;
  }
}

BlogRouter

Et le routeur correspondant pour l’ouverture des pages

import 'package:fracosfr/router/modules/blog.dart';

class BlogRouter {
  static openHome() {
    BlogRouteConfiguration().changeModule(ModuleBlogPage.liste);
  }

  static openArticle(String id) {
    BlogRouteConfiguration().changeModule(ModuleBlogPage.article, id: id);
  }
}

Hello,
Le sujet est intéressant dans mon cas, je ne l’ai pas encore utilisé. Ton schéma qui présente d’une certaine façon l’arborescence de tes écrans avec les Navigators, il y a une chose qui me chagrine.
Si admettons demain ta page contact devenait plus ardue avec d’autres choses derrière, alors dans l’idée il faudrait que tu implémentes après coup un navigator.
S’agit t’il d’un vrai arbre de hiérarchie ou bien peut on peut on passer de n’importe quel écran à l’autre ?

Hello,

Oui tout à fait, j’aime bien un peu tout compartimenter, l’idée étant que si tu fait évoluer un sous module tu n’a pas besoin de toucher aux couches supérieures les liens existants ne bougeant pas, le reste de l’application fonctionnera toujours sans modification (je l’ai éprouvé dans la programmation d’automate industriel, au final c’est hyper pratique).

Les deux mon capitaine, le fonctionnement reste identique à celui présenté dans le cours de @mbritto, sauf que dans mes essais j’ai remplacé le NavigationPath par des classes statiques ce qui évite d’avoir à remonter dans le NavigationDelegate pour gérer les écrans.

Donc oui on peut appeler n’importe quelle page depuis n’importe quel endroit de l’application par exemple en appelant BlogRouter.openArticle("test") depuis la page d’accueil.

Par contre on garde une hiérarchie, en fermant l’article on se retrouve sur la page liste des articles.
Il est possible de passer outre cela en gérant sois même un historique des pages ce qui permettrait en fermant la page article de se retrouver sur la page Home, l’idée serait alors d’avoir un truc du genre

Après ça reste propre à chaque application et comment on souhaite gérer l’ordre des pages.

J’ai un peu de temps aujourd’hui je vais fouiller un peu plus le sujet.

Merci pour ton retour détaillé. Tu l’abordes dans la dernière partie de ton message, effectivement tout dépend aussi si la promenade dans les écrans est statefull ou stateless.
C’est marrant parce que ta hiérarchie ressemble plus à un site web qu’à une appli :slight_smile:

Je fais mes essais de Navigator sur une appli web ça me permet de vérifier les routes plus facilement dans les url :wink:

Une habitude de travail je pense, par expl un extrait d’une app Android en dev :

1 « J'aime »