Navigation et Routing 1.0 dans Flutter

post-thumb

Ce post explique comment les API Navigator et Router marchent. Si vous suivez les design officiels , vous avez peut-être vu la nouvelle spécification pour la Navigation 2.0 et le nouveau système de Router. Cet article ci-dessous vous permettra de mieux comprendre la navigation 1.0.

La nouvelle API Navigation 2.0 et le nouveau système de Router n’introduit pas de breaking changes. Elle rajoute juste une nouvelle API déclarative.

Avant cette nouvelle API, le système de navigation se base sur le principe de push et pop des pages. Ce principe peut très vite devenir contraignant. Toutefois si vous êtes OK avec ce modèle 1.0 dit impératif, il n’y a pas de raison de changer.

Le Router permet de gérer les routes en fonction de la plateforme sur laquelle on fait tourner notre application Flutter. Avant de basculer dans la compréhension de la nouvelle API , il est intéressant de mieux comprendre la version 1.0

Si vous voulez implémenter la navigation 1.0, vous devez être familié avec l’objet Navigator et les concepts suivants :

  • Navigator qui gère la pile (stack) d’objets de type Route
  • Route qui est tout simplement l’objet géré par le Navigator et qui représente la page à afficher. Par exemple , la page peut être implémentée par le MaterialPageRoute

Avec la navigation 1.0, les Routes sont sur le modèle push et pop dans la pile du Navigator , soit sous la forme de routes nommées (named routes) ou de routes anonymes ( anonymous routes)

Les routes anonymes ( anonymous routes)

La plupart des applications mobiles affichent les écrans au dessus des autres, sous forme de pile (stack). Dans Flutter, c’est simple de suivre cette approche en utilisant l’objet Navigator

Les objets MaterialApp et CupertinoApp sont implémentés de telle sorte à utiliser l’objet Navigator :

L’exemple ci-dessous

 1class Screen1 extends StatelessWidget {
 2  @override
 3  Widget build(BuildContext context) {
 4    return Scaffold(
 5      appBar: AppBar(
 6        title: Text('Ecran N°1'),
 7      ),
 8      body: Center(
 9        child: ElevatedButton(
10          child: Text('via le push : Naviguer vers l\'écran N°2'),
11          onPressed: () {
12            Navigator.push(
13              context,
14              MaterialPageRoute(
15                builder: (context) => Screen2(),
16              ),
17            );
18          },
19        ),
20      ),
21    );
22  }
23}
24
25class Screen2 extends StatelessWidget {
26  @override
27  Widget build(BuildContext context) {
28    return Scaffold(
29      appBar: AppBar(
30        title: Text('Ecran N°2'),
31      ),
32      body: Center(
33        child: ElevatedButton(
34          onPressed: () {
35            Navigator.pop(context);
36          },
37          child: Text('via le pop : retour à l\'écran précédent'),
38        ),
39      ),
40    );
41  }
42}

Voici le résultat en action :

Quand la méthode push() est invoquée, le widget Screen2 se place au dessus du widget Screen1 . Lorsque Screen2 s’affiche, le widget Screen1 fait toujours partie de l’arbre des widgets de l’application.

navigator_push_diagram

Les routes nommées (named routes)

Flutter permet aussi de définir des routes nommées.

 1import 'package:flutter/material.dart';
 2
 3void main() {
 4  runApp(MaterialApp(
 5    title: 'Navigation Imperative',
 6    initialRoute: '/',
 7    routes: {
 8      '/': (context) => Screen1(),
 9      '/second': (context) => Screen2(),
10    },
11  ));
12}
13
14class Screen1 extends StatelessWidget {
15  @override
16  Widget build(BuildContext context) {
17    return Scaffold(
18      appBar: AppBar(
19        title: Text('Ecran N°1'),
20      ),
21      body: Center(
22        child: ElevatedButton(
23          child: Text('via le push : Naviguer vers l\'écran N°2'),
24          onPressed: () {
25            Navigator.pushNamed(context, '/second');
26          },
27        ),
28      ),
29    );
30  }
31}
32
33class Screen2 extends StatelessWidget {
34  @override
35  Widget build(BuildContext context) {
36    return Scaffold(
37      appBar: AppBar(
38        title: Text('Ecran N°2'),
39      ),
40      body: Center(
41        child: ElevatedButton(
42          onPressed: () {
43            Navigator.pop(context);
44          },
45          child: Text('via le pop : retour à l\'écran précédent'),
46        ),
47      ),
48    );
49  }
50}

Pour fonctionner dans l’application , les routes nommées doivent être prédéfinies. Bien qu’on puisse passer des arguments aux routes nommées, il est presque impossible de parser ses arguments depuis le mécanisme de navigation.

Les routes nommées avec l’utilisation de onGenerateRoute

Le moyen le plus flexible de manipuler les routes nommées est d’utiliser la méthode onGenerateRoute. Cet API permet de manipuler les paths et parser les arguments passés lors de la navigation.

Dans le code en exemple ci-dessous, lorsqu’on va afficher le Screen2, on va lui passer des arguments dans l’appel de Navigator.push et on peut rajouter de la logique pour manipuler l’uri dans la méthode onGenerateRoute.

settings est une instance de l’objet RouteSettings.

 1import 'package:flutter/material.dart';
 2
 3class ScreenArguments {
 4  final String title;
 5  final String message;
 6
 7  ScreenArguments(this.title, this.message);
 8}
 9
10void main() {
11  runApp(MaterialApp(
12    title: 'Navigation Imperative',
13    initialRoute: '/',
14    routes: {
15      '/': (context) => Screen1(),
16      '/second': (context) => Screen2(),
17    },
18    onGenerateRoute: (settings) {
19      if (settings.name == '/') {
20        return MaterialPageRoute(builder: (context) => Screen1());
21      } else if (settings.name == '/second') {
22        return MaterialPageRoute(
23          builder: (context) {
24            return Screen2();
25          },
26        );
27      }
28
29      return MaterialPageRoute(builder: (context) => UnknownScreen());
30    },
31  ));
32}
33
34class UnknownScreen extends StatelessWidget {
35  @override
36  Widget build(BuildContext context) {
37    return Scaffold(
38      appBar: AppBar(),
39      body: Center(
40        child: Text('404!'),
41      ),
42    );
43  }
44}
45
46class Screen1 extends StatelessWidget {
47  @override
48  Widget build(BuildContext context) {
49    return Scaffold(
50      appBar: AppBar(
51        title: Text('Ecran N°1'),
52      ),
53      body: Center(
54        child: ElevatedButton(
55          child: Text('via le push : Naviguer vers l\'écran N°2'),
56          onPressed: () {
57            Navigator.pushNamed(
58              context,
59              '/second',
60              arguments: ScreenArguments('arg-title', 'arg-message'),
61            );
62          },
63        ),
64      ),
65    );
66  }
67}
68
69class Screen2 extends StatelessWidget {
70  @override
71  Widget build(BuildContext context) {
72    final ScreenArguments args = ModalRoute.of(context).settings.arguments;
73    return Scaffold(
74      appBar: AppBar(
75        title: Text('Ecran N°2'),
76      ),
77      body: Center(
78        child: Column(
79          mainAxisAlignment: MainAxisAlignment.center,
80          children: <Widget>[
81            Text('Arg title is ${args.title}'),
82            Text('Arg message is ${args.message}'),
83            ElevatedButton(
84              onPressed: () {
85                Navigator.pop(context);
86              },
87              child: Text('via le pop : retour à l\'écran précédent'),
88            ),
89          ],
90        ),
91      ),
92    );
93  }
94}

Voici le résultat en action :