Aller au contenu principal

Navigation et Routing

Introduction : Le système de navigation Flutter

Flutter utilise un système de navigation basé sur une pile (stack) pour gérer les écrans :

  • Push : Ajouter un écran au-dessus de la pile
  • Pop : Retirer l'écran actuel et revenir au précédent

Le Navigator gère cette pile automatiquement. La navigation est fournie par le widget app que vous choisissez, et chaque architecture offre des transitions différentes.


1) Navigation selon l'architecture

MaterialApp → MaterialPageRoute

MaterialApp utilise MaterialPageRoute pour créer des routes avec transitions Material Design.

Caractéristiques :

  • Transition slide up (Android)
  • Style automatique selon le theme
  • Animation prédéfinie (300ms)

Exemple :

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accueil')),
body: Center(
child: ElevatedButton(
child: const Text('Voir les détails'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
),
);
},
),
),
);
}
}

class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Détails')),
body: Center(
child: ElevatedButton(
child: const Text('Retour'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}

CupertinoApp → CupertinoPageRoute

CupertinoApp utilise CupertinoPageRoute pour créer des routes avec transitions iOS natives.

Caractéristiques :

  • Transition slide from right (iOS)
  • Style iOS automatique
  • Geste de retour natif (swipe)

Exemple :

import 'package:flutter/cupertino.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});


Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Accueil'),
),
child: Center(
child: CupertinoButton.filled(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => const DetailScreen(),
),
);
},
child: const Text('Voir les détails'),
),
),
);
}
}

class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});


Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Détails'),
),
child: Center(
child: CupertinoButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour'),
),
),
);
}
}

WidgetsApp / Custom → PageRouteBuilder

PageRouteBuilder permet de créer des routes avec animations personnalisées. Utilisé avec WidgetsApp ou quand vous voulez un contrôle total.

Définition : PageRouteBuilder vous donne un contrôle total sur :

  • Le type de transition (slide, fade, scale, rotation, etc.)
  • La durée de l'animation
  • La courbe d'animation (easing)
  • Les animations combinées

Paramètres principaux :

  • pageBuilder : Construit l'écran de destination
  • transitionsBuilder : Définit l'animation de transition
  • transitionDuration : Durée de l'animation (par défaut 300ms)

Exemple - Slide transition :

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Slide Transition')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0); // Depuis la droite
const end = Offset.zero;
const curve = Curves.easeInOut;

var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);

return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
},
child: const Text('Avec animation slide'),
),
),
);
}
}

class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Détails')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour'),
),
),
);
}
}

Exemple - Fade transition :

Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);

Exemple - Scale transition :

Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: animation,
child: child,
);
},
),
);

2) Passage de données entre écrans

Envoyer des données à un nouvel écran

Passez des données via le constructeur de l'écran de destination.

class Todo {
final String title;
final String description;

Todo(this.title, this.description);
}

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(
todo: Todo('Titre', 'Description détaillée'),
),
),
);
},
child: const Text('Voir Todo'),
),
),
);
}
}

// Écran de destination
class DetailScreen extends StatelessWidget {
final Todo todo;

const DetailScreen({super.key, required this.todo});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(todo.title)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text(todo.description),
),
);
}
}

Retourner des données à l'écran appelant

Lorsqu'un écran fait pop(), il peut retourner une valeur à l'écran qui l'a ouvert. L'écran appelant utilise await avec Navigator.push() pour recevoir cette valeur.

Fonctionnement :

  1. L'écran A ouvre l'écran B avec Navigator.push()
  2. L'écran A attend (await) le retour de B
  3. L'écran B fait Navigator.pop(context, valeur) pour retourner une valeur
  4. L'écran A reçoit cette valeur et peut l'utiliser

Exemple :

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accueil')),
body: Center(
child: ElevatedButton(
onPressed: () async {
// Écran d'origine : attendre le résultat
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SelectionScreen(),
),
);

// Utiliser le résultat
if (result != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Résultat : $result')),
);
}
},
child: const Text('Sélectionner'),
),
),
);
}
}

// Écran de sélection : retourner des données
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sélection')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Option A');
},
child: const Text('Option A'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Option B'),
},
child: const Text('Option B'),
),
],
),
),
);
}
}

Exemple complet : Liste vers détail avec retour de données

class Person {
final String name;
final String email;
final int age;

Person(this.name, this.email, this.age);
}

class PersonListScreen extends StatelessWidget {
final List<Person> people = [
Person('Alice', 'alice@example.com', 25),
Person('Bob', 'bob@example.com', 30),
Person('Charlie', 'charlie@example.com', 35),
];

PersonListScreen({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Liste de personnes')),
body: ListView.builder(
itemCount: people.length,
itemBuilder: (context, index) {
final person = people[index];
return ListTile(
title: Text(person.name),
subtitle: Text(person.email),
trailing: const Icon(Icons.arrow_forward),
onTap: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => PersonDetailScreen(person: person),
),
);

if (result != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(result)),
);
}
},
);
},
),
);
}
}

class PersonDetailScreen extends StatelessWidget {
final Person person;

const PersonDetailScreen({super.key, required this.person});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(person.name)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Nom : ${person.name}', style: const TextStyle(fontSize: 20)),
const SizedBox(height: 8),
Text('Email : ${person.email}', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text('Âge : ${person.age} ans', style: const TextStyle(fontSize: 16)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Détails consultés pour ${person.name}');
},
child: const Text('Retour avec message'),
),
],
),
),
);
}
}

3) Navigation avancée

pushReplacement : Remplacer l'écran actuel

Usage : Remplace l'écran actuel au lieu de l'empiler. Utile pour les écrans de connexion ou d'onboarding.

Différence avec push :

  • push : Écran A → Écran B (A reste dans la pile)
  • pushReplacement : Écran A → Écran B (A est retiré de la pile)
// L'écran de login est remplacé par l'écran d'accueil
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
);

Cas d'usage typiques :

  • Login → Home (après authentification réussie)
  • Onboarding → Home (après avoir terminé le tutoriel)
  • Splash → Home (après chargement)

pushAndRemoveUntil : Nettoyer la pile

Usage : Navigue vers un nouvel écran et retire tous les écrans précédents selon une condition.

// Retour à l'écran d'accueil, supprime tout l'historique
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
(route) => false, // Supprime tous les écrans
);

Cas d'usage typiques :

  • Logout → Login (vider toute la pile de navigation)
  • Réinitialiser l'app après une action critique

popUntil : Revenir jusqu'à un écran spécifique

Usage : Retire plusieurs écrans de la pile jusqu'à atteindre une condition.

// Revenir à l'écran d'accueil (premier écran de la pile)
Navigator.popUntil(context, (route) => route.isFirst);

Exemple avec route nommée :

// Revenir à un écran spécifique
Navigator.popUntil(context, ModalRoute.withName('/home'));

canPop : Vérifier si on peut revenir en arrière

Usage : Vérifie s'il y a des écrans dans la pile avant de faire pop().

if (Navigator.canPop(context)) {
Navigator.pop(context);
} else {
// On est sur l'écran racine
print('Impossible de revenir en arrière');
}

Cas d'usage typiques :

  • Éviter les erreurs quand l'utilisateur est sur l'écran racine
  • Gérer le comportement du bouton retour personnalisé

WillPopScope : Intercepter le bouton retour

Usage : Contrôler le comportement du bouton retour système (Android) ou du geste de retour (iOS).

Cas d'usage typiques :

  • Confirmation avant de quitter
  • Sauvegarder avant de quitter
  • Bloquer le retour dans certaines situations

Exemple - Confirmation de sortie :

class ExitConfirmationScreen extends StatelessWidget {
const ExitConfirmationScreen({super.key});

Future<bool> _onWillPop(BuildContext context) async {
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmation'),
content: const Text('Voulez-vous vraiment quitter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Non'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Oui'),
),
],
),
);
return shouldPop ?? false;
}


Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => _onWillPop(context),
child: Scaffold(
appBar: AppBar(title: const Text('Écran')),
body: const Center(child: Text('Contenu')),
),
);
}
}

Exemple - Bloquer le retour :

WillPopScope(
onWillPop: () async => false, // Bloque complètement le retour
child: Scaffold(
appBar: AppBar(title: const Text('Écran bloqué')),
body: const Center(child: Text('Impossible de revenir en arrière')),
),
);

Bonnes pratiques

  1. Toujours vérifier context.mounted après un await Navigator avant d'utiliser le context
  2. Typage générique : Navigator.push<ReturnType>() pour typer le retour
  3. pushReplacement pour les écrans de connexion/logout
  4. WillPopScope pour les confirmations de sortie
  5. Passer des objets plutôt que des valeurs primitives pour plus de flexibilité
  6. canPop avant de faire un pop() pour éviter les erreurs
  7. Choisir l'architecture appropriée : MaterialApp pour Android, CupertinoApp pour iOS, PageRouteBuilder pour custom