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 destinationtransitionsBuilder: Définit l'animation de transitiontransitionDuration: 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 :
- L'écran A ouvre l'écran B avec
Navigator.push() - L'écran A attend (
await) le retour de B - L'écran B fait
Navigator.pop(context, valeur)pour retourner une valeur - 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
- Toujours vérifier context.mounted après un
await Navigatoravant d'utiliser le context - Typage générique :
Navigator.push<ReturnType>()pour typer le retour - pushReplacement pour les écrans de connexion/logout
- WillPopScope pour les confirmations de sortie
- Passer des objets plutôt que des valeurs primitives pour plus de flexibilité
- canPop avant de faire un
pop()pour éviter les erreurs - Choisir l'architecture appropriée : MaterialApp pour Android, CupertinoApp pour iOS, PageRouteBuilder pour custom