Routes nommées
Les routes nommées permettent de naviguer vers des écrans en utilisant des identifiants string plutôt que de créer des instances de widgets. Cela simplifie la navigation et centralise la configuration des routes.
Configuration des routes
Définir les routes dans MaterialApp
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accueil')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/detail'),
child: const Text('Détails'),
),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/profile'),
child: const Text('Profil'),
),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/settings'),
child: const Text('Paramètres'),
),
],
),
),
);
}
}
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'),
),
),
);
}
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profil')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour'),
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Paramètres')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour'),
),
),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigation Demo',
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
'/profile': (context) => const ProfileScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
}
}
Navigation avec routes nommées
// Naviguer vers une route nommée
Navigator.pushNamed(context, '/detail');
// Naviguer et remplacer
Navigator.pushReplacementNamed(context, '/');
// Retour à une route spécifique
Navigator.popAndPushNamed(context, '/');
Passage d'arguments avec routes nommées
Envoyer et récupérer des arguments
Envoi : Utiliser le paramètre arguments de pushNamed()
Réception : Récupérer via ModalRoute.of(context)?.settings.arguments (avec vérification)
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(
onPressed: () {
Navigator.pushNamed(
context,
'/detail',
arguments: {'id': 123, 'title': 'Mon titre'},
);
},
child: const Text('Voir les détails'),
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
Widget build(BuildContext context) {
// Récupérer les arguments avec vérification
final args = ModalRoute.of(context)?.settings.arguments;
if (args == null || args is! Map<String, dynamic>) {
return Scaffold(
appBar: AppBar(title: const Text('Erreur')),
body: const Center(child: Text('Arguments invalides')),
);
}
final id = args['id'] as int;
final title = args['title'] as String;
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('ID: $id'),
),
);
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
},
);
}
}
Classe d'arguments typée
Meilleure pratique : Créer une classe dédiée pour garantir la sécurité des types.
import 'package:flutter/material.dart';
class DetailArguments {
final int id;
final String title;
final String? description;
DetailArguments({
required this.id,
required this.title,
this.description,
});
}
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: () {
Navigator.pushNamed(
context,
'/detail',
arguments: DetailArguments(
id: 123,
title: 'Mon titre',
description: 'Description optionnelle',
),
);
},
child: const Text('Voir les détails'),
),
),
);
}
}
// Réception
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as DetailArguments;
return Scaffold(
appBar: AppBar(title: Text(args.title)),
body: Column(
children: [
Text('ID: ${args.id}'),
if (args.description != null)
Text('Description: ${args.description}'),
],
),
);
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
},
);
}
}
onGenerateRoute : Routes dynamiques
Qu'est-ce que onGenerateRoute ?
onGenerateRoute est un callback appelé par Flutter lorsqu'une route n'est pas trouvée dans le dictionnaire routes.
Différence avec routes statiques :
routes | onGenerateRoute |
|---|---|
Dictionnaire fixe : '/detail': (context) => DetailScreen() | Fonction qui génère des routes à la demande |
| Routes statiques sans paramètres | Routes dynamiques avec paramètres (ex: /user/123) |
| Simple, limité | Flexible, puissant |
Cas d'usage typiques :
- Routes avec IDs :
/user/123,/product/456 - Routes avec slugs :
/article/mon-article - Parsing complexe d'URL
- Validation d'arguments avant navigation
Fonctionnement :
Navigator.pushNamed(context, '/user/123')est appelé- Flutter cherche
/user/123dansroutes→ pas trouvé - Flutter appelle
onGenerateRouteavecRouteSettings(name: '/user/123') - Votre fonction parse l'URL, extrait l'ID et retourne la route appropriée
Exemple : Routes avec paramètres d'URL
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: () => Navigator.pushNamed(context, '/user/123'),
child: const Text('Voir utilisateur 123'),
),
),
);
}
}
class UserScreen extends StatelessWidget {
final int userId;
const UserScreen({super.key, required this.userId});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Utilisateur')),
body: Center(
child: Text('ID utilisateur: $userId'),
),
);
}
}
class DetailScreen extends StatelessWidget {
final DetailArguments? arguments;
const DetailScreen({super.key, this.arguments});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Détails')),
body: const Center(child: Text('Détails')),
);
}
}
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(child: Text('Page non trouvée')),
);
}
}
class DetailArguments {
final int id;
final String title;
DetailArguments({required this.id, required this.title});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigation Demo',
onGenerateRoute: (settings) {
// Récupérer le nom de la route
final uri = Uri.parse(settings.name ?? '/');
// Route racine
if (uri.path == '/') {
return MaterialPageRoute(
builder: (context) => const HomeScreen(),
);
}
// Route avec paramètre : /user/123
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'user') {
final userId = int.tryParse(uri.pathSegments[1]);
if (userId != null) {
return MaterialPageRoute(
builder: (context) => UserScreen(userId: userId),
settings: settings,
);
}
}
// Route detail avec arguments
if (uri.path == '/detail') {
final args = settings.arguments as DetailArguments?;
if (args != null) {
return MaterialPageRoute(
builder: (context) => DetailScreen(arguments: args),
);
}
}
// Route 404
return MaterialPageRoute(
builder: (context) => const NotFoundScreen(),
);
},
);
}
}
onUnknownRoute : Gestion du 404
Définir une route par défaut pour les routes inconnues.
MaterialApp(
onGenerateRoute: (settings) {
// Logique de routing
},
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(
child: Text('Page non trouvée'),
),
),
);
},
)
Constantes de routes
Centraliser les noms de routes pour éviter les erreurs de typage.
class AppRoutes {
static const String home = '/';
static const String detail = '/detail';
static const String profile = '/profile';
static const String settings = '/settings';
static const String login = '/login';
// Route avec paramètres
static String user(int id) => '/user/$id';
static String product(String slug) => '/product/$slug';
}
// Utilisation
Navigator.pushNamed(context, AppRoutes.detail);
Navigator.pushNamed(context, AppRoutes.user(123));
Router avancé avec paramètres
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: () => Navigator.pushNamed(context, '/detail'),
child: const Text('Voir les détails'),
),
),
);
}
}
class DetailArguments {
final int id;
final String title;
DetailArguments({required this.id, required this.title});
}
class DetailScreen extends StatelessWidget {
final DetailArguments arguments;
const DetailScreen({super.key, required this.arguments});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(arguments.title)),
body: Center(child: Text('ID: ${arguments.id}')),
);
}
}
class UserScreen extends StatelessWidget {
final int userId;
const UserScreen({super.key, required this.userId});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Utilisateur')),
body: Center(child: Text('Utilisateur ID: $userId')),
);
}
}
class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
final uri = Uri.parse(settings.name ?? '/');
// Route avec paramètre : /user/123
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'user') {
final userId = int.tryParse(uri.pathSegments[1]);
if (userId != null) {
return MaterialPageRoute(
builder: (_) => UserScreen(userId: userId),
);
}
return _errorRoute('ID utilisateur invalide');
}
switch (uri.path) {
case '/':
return MaterialPageRoute(builder: (_) => const HomeScreen());
case '/detail':
final args = settings.arguments;
if (args is DetailArguments) {
return MaterialPageRoute(
builder: (_) => DetailScreen(arguments: args),
);
}
return _errorRoute('Arguments invalides pour /detail');
default:
return _errorRoute('Route ${uri.path} non trouvée');
}
}
static Route<dynamic> _errorRoute(String message) {
return MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('Erreur')),
body: Center(child: Text(message)),
),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: AppRouter.generateRoute,
);
}
}
Exemple complet avec routes nommées
// Constantes
class Routes {
static const home = '/';
static const productList = '/products';
static const productDetail = '/products/detail';
static const cart = '/cart';
}
// Arguments
class ProductDetailArgs {
final int productId;
final String productName;
ProductDetailArgs({required this.productId, required this.productName});
}
// Configuration
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: Routes.home,
routes: {
Routes.home: (context) => const HomeScreen(),
Routes.productList: (context) => const ProductListScreen(),
Routes.cart: (context) => const CartScreen(),
},
onGenerateRoute: (settings) {
if (settings.name == Routes.productDetail) {
final args = settings.arguments as ProductDetailArgs?;
if (args != null) {
return MaterialPageRoute(
builder: (context) => ProductDetailScreen(args: args),
);
}
}
return null;
},
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => const NotFoundScreen(),
);
},
);
}
}
// Écran d'accueil
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: () => Navigator.pushNamed(context, Routes.productList),
child: const Text('Voir les produits'),
),
),
);
}
}
// Écran liste
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Produits')),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Produit $index'),
onTap: () {
Navigator.pushNamed(
context,
Routes.productDetail,
arguments: ProductDetailArgs(
productId: index,
productName: 'Produit $index',
),
);
},
);
},
),
);
}
}
// Écran détail
class ProductDetailScreen extends StatelessWidget {
final ProductDetailArgs args;
const ProductDetailScreen({super.key, required this.args});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(args.productName)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ID: ${args.productId}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, Routes.cart);
},
child: const Text('Ajouter au panier'),
),
],
),
),
);
}
}
// Écran panier
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Panier')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour'),
),
),
);
}
}
// Écran 404
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(
child: Text('Page non trouvée'),
),
);
}
}
Bonnes pratiques
-
Constantes : Utiliser des constantes pour les noms de routes
class AppRoutes {static const home = '/';static const detail = '/detail';} -
Classes d'arguments : Créer des classes typées pour les arguments
class DetailArguments {final int id;final String title;DetailArguments({required this.id, required this.title});} -
onGenerateRoute : Pour les routes dynamiques avec paramètres
- Routes avec IDs :
/user/123 - Parsing d'URL :
Uri.parse(settings.name) - Validation des paramètres
- Routes avec IDs :
-
onUnknownRoute : Toujours gérer les routes inconnues (404)
onUnknownRoute: (settings) => MaterialPageRoute(builder: (_) => NotFoundScreen(),) -
Validation : Valider les arguments avant de construire l'écran
if (args is DetailArguments) {return MaterialPageRoute(builder: (_) => DetailScreen(args: args));}return errorRoute(); -
Centralisation : Regrouper la logique de routing dans une classe dédiée
class AppRouter {static Route<dynamic> generateRoute(RouteSettings settings) {// Toute la logique ici}} -
Type safety : Éviter les casts non sécurisés
- Utiliser
ispour vérifier le type - Utiliser
as?pour cast nullable - Gérer les cas
null
- Utiliser
-
Documentation : Documenter toutes les routes et leurs arguments attendus
-
Nommage : Utiliser des conventions claires
- Routes :
/user/profile(kebab-case) - Constantes :
AppRoutes.userProfile(camelCase)
- Routes :
-
Séparation : Séparer la configuration des routes de la logique métier