Skip to content

Named Routes

Understand how to organize navigation with named routes in Flutter.


What is it?

Named Routes are a way to define and navigate to screens using string identifiers instead of building routes directly. They provide a centralized way to manage navigation, making your code more organized, maintainable, and easier to handle deep linking and complex navigation flows.


Why does it exist?

Named Routes exist to:

  • Centralize route definitions
  • Simplify navigation code
  • Support deep linking
  • Enable dynamic route generation
  • Organize app navigation
  • Handle unknown routes
  • Pass data with navigation

Defining Named Routes

Named routes are defined in MaterialApp.

// Defining named routes
void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes',
      // 1. Define initial route
      initialRoute: '/',

      // 2. Define routes map
      routes: {
        '/': (context) => const HomeScreen(),
        '/profile': (context) => const ProfileScreen(),
        '/settings': (context) => const SettingsScreen(),
        '/about': (context) => const AboutScreen(),
      },

      // 3. Handle unknown routes
      onUnknownRoute: (settings) {
        return MaterialPageRoute(
          builder: (context) => const NotFoundScreen(),
        );
      },
    );
  }
}

// Using named routes
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Navigate using pushNamed
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/profile');
              },
              child: const Text('Go to Profile'),
            ),

            // Navigate and replace
            ElevatedButton(
              onPressed: () {
                Navigator.pushReplacementNamed(context, '/settings');
              },
              child: const Text('Replace with Settings'),
            ),

            // Navigate and remove all
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamedAndRemoveUntil(
                  context,
                  '/about',
                  (route) => false,
                );
              },
              child: const Text('Go to About (Clear Stack)'),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - routes map defines named routes - initialRoute sets starting screen - pushNamed navigates to named route - pushReplacementNamed replaces current - pushNamedAndRemoveUntil clears stack


Named Routes with Arguments

Passing data to named routes.

// Named routes with arguments
class NamedRoutesWithArgs extends StatelessWidget {
  const NamedRoutesWithArgs({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes with Args',
      initialRoute: '/',
      routes: {
        '/': (context) => const NamedHome(),
        '/user': (context) => const UserDetailScreen(),
        '/product': (context) => const ProductDetailScreen(),
      },
      // Handle routes with arguments
      onGenerateRoute: (settings) {
        if (settings.name == '/user') {
          final args = settings.arguments as Map<String, dynamic>?;
          return MaterialPageRoute(
            builder: (context) => UserDetailScreen(
              userId: args?['id'] ?? 0,
              userName: args?['name'] ?? 'Unknown',
            ),
          );
        }
        if (settings.name == '/product') {
          final args = settings.arguments as ProductArgs?;
          return MaterialPageRoute(
            builder: (context) => ProductDetailScreen(
              product: args!,
            ),
          );
        }
        return null;
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Pass arguments as map
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(
                  context,
                  '/user',
                  arguments: {
                    'id': 123,
                    'name': 'John Doe',
                  },
                );
              },
              child: const Text('Go to User (Map)'),
            ),

            // Pass arguments as object
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(
                  context,
                  '/product',
                  arguments: ProductArgs(
                    id: 456,
                    name: 'Laptop',
                    price: 999.99,
                  ),
                );
              },
              child: const Text('Go to Product (Object)'),
            ),
          ],
        ),
      ),
    );
  }
}

class UserDetailScreen extends StatelessWidget {
  const UserDetailScreen({
    super.key,
    required this.userId,
    required this.userName,
  });

  final int userId;
  final String userName;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User $userId')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text('ID: $userId'),
            Text('Name: $userName'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

// Type-safe arguments object
class ProductArgs {
  final int id;
  final String name;
  final double price;

  ProductArgs({
    required this.id,
    required this.name,
    required this.price,
  });
}

class ProductDetailScreen extends StatelessWidget {
  const ProductDetailScreen({
    super.key,
    required this.product,
  });

  final ProductArgs product;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product ${product.id}')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text('ID: ${product.id}'),
            Text('Name: ${product.name}'),
            Text('Price: \$${product.price}'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - arguments parameter passes data - onGenerateRoute extracts arguments - Type-safe arguments objects - Map for dynamic arguments


onGenerateRoute

Advanced route generation with onGenerateRoute.

// Using onGenerateRoute for dynamic routing
class DynamicRoutesApp extends StatelessWidget {
  const DynamicRoutesApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamic Routes',
      initialRoute: '/',
      onGenerateRoute: (settings) {
        // Parse route name
        final uri = Uri.parse(settings.name ?? '');
        final pathSegments = uri.pathSegments;

        // Handle dynamic routes
        if (pathSegments.isNotEmpty) {
          // /user/123
          if (pathSegments[0] == 'user' && pathSegments.length == 2) {
            final id = int.tryParse(pathSegments[1]) ?? 0;
            return MaterialPageRoute(
              builder: (context) => DynamicUserScreen(userId: id),
              settings: settings,
            );
          }

          // /product/123/detail
          if (pathSegments[0] == 'product' && pathSegments.length == 3) {
            final id = int.tryParse(pathSegments[1]) ?? 0;
            final action = pathSegments[2];
            return MaterialPageRoute(
              builder: (context) => DynamicProductScreen(
                productId: id,
                action: action,
              ),
              settings: settings,
            );
          }
        }

        // Static routes
        switch (settings.name) {
          case '/':
            return MaterialPageRoute(
              builder: (context) => const DynamicHomeScreen(),
            );
          case '/about':
            return MaterialPageRoute(
              builder: (context) => const AboutScreen(),
            );
        }

        // Unknown route
        return MaterialPageRoute(
          builder: (context) => const NotFoundScreen(),
        );
      },
      onUnknownRoute: (settings) {
        return MaterialPageRoute(
          builder: (context) => const NotFoundScreen(),
        );
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dynamic Routes')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // Navigate to /user/123
                Navigator.pushNamed(context, '/user/123');
              },
              child: const Text('Go to User 123'),
            ),
            ElevatedButton(
              onPressed: () {
                // Navigate to /product/456/detail
                Navigator.pushNamed(context, '/product/456/detail');
              },
              child: const Text('Go to Product 456 Detail'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/about');
              },
              child: const Text('About'),
            ),
          ],
        ),
      ),
    );
  }
}

class DynamicUserScreen extends StatelessWidget {
  const DynamicUserScreen({super.key, required this.userId});

  final int userId;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User $userId')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('User ID: $userId'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

class DynamicProductScreen extends StatelessWidget {
  const DynamicProductScreen({
    super.key,
    required this.productId,
    required this.action,
  });

  final int productId;
  final String action;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product $productId')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Product ID: $productId'),
            Text('Action: $action'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - Parse URI for dynamic paths - Extract parameters from path - Handle complex routing patterns - Support deep linking


Navigation with Named Routes

Advanced navigation using named routes.

// Advanced navigation with named routes
class AdvancedNamedNavigation extends StatelessWidget {
  const AdvancedNamedNavigation({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Advanced Navigation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 1. Push named route
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/next');
              },
              child: const Text('Push Named'),
            ),

            // 2. Push named and remove all
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamedAndRemoveUntil(
                  context,
                  '/home',
                  (route) => false,
                );
              },
              child: const Text('Push Named and Remove All'),
            ),

            // 3. Push named with predicate
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamedAndRemoveUntil(
                  context,
                  '/profile',
                  (route) => route.settings.name == '/home',
                );
              },
              child: const Text('Push Named (Keep Home)'),
            ),

            // 4. Pop until named route
            ElevatedButton(
              onPressed: () {
                Navigator.popUntil(
                  context,
                  (route) => route.settings.name == '/home',
                );
              },
              child: const Text('Pop Until Home'),
            ),

            // 5. Check if can pop
            ElevatedButton(
              onPressed: () {
                if (Navigator.canPop(context)) {
                  Navigator.pop(context);
                }
              },
              child: const Text('Pop if Possible'),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - pushNamedAndRemoveUntil: clear stack - popUntil: pop to specific route - canPop: check if can pop - Route settings for identification


Real-World Examples

Common patterns with named routes.

// 1. Route constants and organization
class AppRoutes {
  static const String home = '/';
  static const String login = '/login';
  static const String register = '/register';
  static const String forgotPassword = '/forgot-password';
  static const String profile = '/profile';
  static const String settings = '/settings';
  static const String product = '/product';
  static const String cart = '/cart';
  static const String checkout = '/checkout';
  static const String orderConfirmation = '/order-confirmation';

  // Dynamic routes
  static String productDetail(int id) => '/product/$id';
  static String userProfile(int id) => '/user/$id';
  static String orderDetails(int id) => '/order/$id';
}

// 2. App with organized routes
class OrganizedApp extends StatelessWidget {
  const OrganizedApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Organized Routes',
      initialRoute: AppRoutes.home,
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case AppRoutes.home:
            return MaterialPageRoute(builder: (_) => const HomeScreen());
          case AppRoutes.login:
            return MaterialPageRoute(builder: (_) => const LoginScreen());
          case AppRoutes.register:
            return MaterialPageRoute(builder: (_) => const RegisterScreen());
          case AppRoutes.profile:
            return MaterialPageRoute(builder: (_) => const ProfileScreen());
          case AppRoutes.settings:
            return MaterialPageRoute(builder: (_) => const SettingsScreen());
          case AppRoutes.cart:
            return MaterialPageRoute(builder: (_) => const CartScreen());
          case AppRoutes.checkout:
            return MaterialPageRoute(builder: (_) => const CheckoutScreen());
        }

        // Dynamic routes
        final uri = Uri.parse(settings.name ?? '');
        if (uri.pathSegments.length == 2) {
          if (uri.pathSegments[0] == 'product') {
            final id = int.tryParse(uri.pathSegments[1]) ?? 0;
            return MaterialPageRoute(
              builder: (_) => ProductDetailScreen(productId: id),
            );
          }
          if (uri.pathSegments[0] == 'user') {
            final id = int.tryParse(uri.pathSegments[1]) ?? 0;
            return MaterialPageRoute(
              builder: (_) => UserProfileScreen(userId: id),
            );
          }
        }

        return MaterialPageRoute(builder: (_) => const NotFoundScreen());
      },
    );
  }
}

// 3. Using route constants
class RouteUsageExample extends StatelessWidget {
  const RouteUsageExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Route Usage')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('Home'),
            onTap: () {
              Navigator.pushNamed(context, AppRoutes.home);
            },
          ),
          ListTile(
            title: const Text('Profile'),
            onTap: () {
              Navigator.pushNamed(context, AppRoutes.profile);
            },
          ),
          ListTile(
            title: const Text('Settings'),
            onTap: () {
              Navigator.pushNamed(context, AppRoutes.settings);
            },
          ),
          ListTile(
            title: const Text('Product 123'),
            onTap: () {
              Navigator.pushNamed(
                context,
                AppRoutes.productDetail(123),
              );
            },
          ),
          ListTile(
            title: const Text('User 456'),
            onTap: () {
              Navigator.pushNamed(
                context,
                AppRoutes.userProfile(456),
              );
            },
          ),
        ],
      ),
    );
  }
}

What's happening here? - Route constants for organization - Dynamic route generation - Centralized route management - Type-safe navigation


Best Practices

Use Route Constants

// Good - Centralized route names
class Routes {
  static const String home = '/';
  static const String profile = '/profile';
  static const String settings = '/settings';
}

// Bad - Hardcoded strings
Navigator.pushNamed(context, '/profile'); // Hardcoded

Organize Route Definitions

// Good - Centralized route generation
MaterialApp(
  onGenerateRoute: RouteGenerator.generateRoute,
)

class RouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    // All route logic here
  }
}

Use Type-Safe Navigation

// Good - Type-safe navigation
class NavigationService {
  void navigateToUser(int userId) {
    Navigator.pushNamed(
      context,
      Routes.user,
      arguments: UserArgs(id: userId),
    );
  }
}

// Bad - Unstructured navigation
Navigator.pushNamed(
  context,
  '/user',
  arguments: userId, // Unclear what's expected
)

Common Mistakes

Duplicate Route Names

Wrong:

// Duplicate definitions
routes: {
  '/profile': (context) => const ProfileScreen(),
  '/profile': (context) => const AnotherScreen(), // Duplicate
}

Correct:

// Unique routes
routes: {
  '/profile': (context) => const ProfileScreen(),
  '/settings': (context) => const SettingsScreen(),
}

Missing Route Handler

Wrong:

// Route not defined
Navigator.pushNamed(context, '/missing'); // Error

Correct:

// Handle unknown routes
onUnknownRoute: (settings) {
  return MaterialPageRoute(
    builder: (context) => const NotFoundScreen(),
  );
},


Summary

Named routes provide centralized navigation management using string identifiers. Define routes in MaterialApp, pass arguments with navigation, and use onGenerateRoute for dynamic routing. Organize route names in constants for maintainability.


Next Steps


Did You Know?

  • Named routes are defined in MaterialApp
  • pushNamed navigates to named routes
  • arguments pass data to routes
  • onGenerateRoute handles dynamic routes
  • Route constants improve maintainability
  • onUnknownRoute handles missing routes
  • Named routes support deep linking
  • Routes can be nested for complex flows