Aller au contenu principal

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(),
},
);
}
}
// 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 :

routesonGenerateRoute
Dictionnaire fixe : '/detail': (context) => DetailScreen()Fonction qui génère des routes à la demande
Routes statiques sans paramètresRoutes 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 :

  1. Navigator.pushNamed(context, '/user/123') est appelé
  2. Flutter cherche /user/123 dans routespas trouvé
  3. Flutter appelle onGenerateRoute avec RouteSettings(name: '/user/123')
  4. 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

  1. Constantes : Utiliser des constantes pour les noms de routes

    class AppRoutes {
    static const home = '/';
    static const detail = '/detail';
    }
  2. 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});
    }
  3. onGenerateRoute : Pour les routes dynamiques avec paramètres

    • Routes avec IDs : /user/123
    • Parsing d'URL : Uri.parse(settings.name)
    • Validation des paramètres
  4. onUnknownRoute : Toujours gérer les routes inconnues (404)

    onUnknownRoute: (settings) => MaterialPageRoute(
    builder: (_) => NotFoundScreen(),
    )
  5. Validation : Valider les arguments avant de construire l'écran

    if (args is DetailArguments) {
    return MaterialPageRoute(builder: (_) => DetailScreen(args: args));
    }
    return errorRoute();
  6. Centralisation : Regrouper la logique de routing dans une classe dédiée

    class AppRouter {
    static Route<dynamic> generateRoute(RouteSettings settings) {
    // Toute la logique ici
    }
    }
  7. Type safety : Éviter les casts non sécurisés

    • Utiliser is pour vérifier le type
    • Utiliser as? pour cast nullable
    • Gérer les cas null
  8. Documentation : Documenter toutes les routes et leurs arguments attendus

  9. Nommage : Utiliser des conventions claires

    • Routes : /user/profile (kebab-case)
    • Constantes : AppRoutes.userProfile (camelCase)
  10. Séparation : Séparer la configuration des routes de la logique métier