Question #35MediumFlutter Basics

Flutter navigation and types

#flutter#navigation

Answer

Overview

Flutter provides a powerful navigation system to move between screens (routes). Understanding navigation types and patterns is essential for building multi-screen apps.


Types of Navigation

TypeDescriptionUse Case
Named RoutesPre-defined routes with string identifiersLarge apps with many screens
Anonymous RoutesRoutes created on-the-flySimple navigation, passing complex data
Generated RoutesProgrammatically generated routesDynamic routing with parameters
Nested NavigationNavigator within NavigatorTab bars, bottom navigation
Deep LinkingExternal URLs open specific screensWeb integration, push notifications

1. Anonymous Routes

Create routes dynamically using

text
MaterialPageRoute
.

dart
// Navigate to new screen
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailsScreen(userId: 123),
  ),
);

// Go back
Navigator.pop(context);

Complete Example

dart
class HomeScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => DetailsScreen(id: 42),
              ),
            );
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

class DetailsScreen extends StatelessWidget {
  final int id;
  
  const DetailsScreen({required this.id});
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details $id')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Go Back'),
        ),
      ),
    );
  }
}

Pros:

  • ✅ Type-safe parameter passing
  • ✅ Simple for few screens
  • ✅ Easy to understand

Cons:

  • ❌ Decentralized (routes scattered)
  • ❌ Harder to maintain in large apps

2. Named Routes

Define all routes in one central location.

dart
void main() {
  runApp(
    MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/profile': (context) => ProfileScreen(),
        '/settings': (context) => SettingsScreen(),
        '/details': (context) => DetailsScreen(),
      },
    ),
  );
}

// Navigate
Navigator.pushNamed(context, '/profile');

// Go back
Navigator.pop(context);

With Arguments

dart
// Navigate with arguments
Navigator.pushNamed(
  context,
  '/profile',
  arguments: {'userId': 123, 'name': 'John'},
);

// Receive arguments
class ProfileScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as Map;
    final userId = args['userId'];
    final name = args['name'];
    
    return Scaffold(
      appBar: AppBar(title: Text('Profile: $name')),
      body: Text('User ID: $userId'),
    );
  }
}

Pros:

  • ✅ Centralized route management
  • ✅ Easy to track navigation flow
  • ✅ Clean navigation code

Cons:

  • ❌ Not type-safe (arguments are dynamic)
  • ❌ Complex parameter passing

3. Generated Routes (onGenerateRoute)

Programmatically create routes with type-safe parameters.

dart
class AppRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => HomeScreen());
      
      case '/profile':
        final args = settings.arguments as ProfileArguments;
        return MaterialPageRoute(
          builder: (_) => ProfileScreen(
            userId: args.userId,
            name: args.name,
          ),
        );
      
      case '/details':
        if (settings.arguments is int) {
          final id = settings.arguments as int;
          return MaterialPageRoute(
            builder: (_) => DetailsScreen(id: id),
          );
        }
        return _errorRoute();
      
      default:
        return _errorRoute();
    }
  }
  
  static Route<dynamic> _errorRoute() {
    return MaterialPageRoute(
      builder: (_) => Scaffold(
        body: Center(child: Text('Route not found')),
      ),
    );
  }
}

// Arguments class
class ProfileArguments {
  final int userId;
  final String name;
  
  ProfileArguments({required this.userId, required this.name});
}

// MaterialApp
MaterialApp(
  onGenerateRoute: AppRouter.generateRoute,
)

// Navigate
Navigator.pushNamed(
  context,
  '/profile',
  arguments: ProfileArguments(userId: 123, name: 'John'),
);

Pros:

  • ✅ Type-safe arguments
  • ✅ Centralized logic
  • ✅ Custom route handling
  • ✅ Error routes (404 pages)

Cons:

  • ❌ More boilerplate code

4. Nested Navigation

Navigator inside another Navigator (for TabBar, BottomNavigationBar).

dart
class HomeScreen extends StatefulWidget {
  
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;
  
  final List<GlobalKey<NavigatorState>> _navigatorKeys = [
    GlobalKey<NavigatorState>(),
    GlobalKey<NavigatorState>(),
    GlobalKey<NavigatorState>(),
  ];
  
  
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // Pop from current tab's navigator
        return !await _navigatorKeys[_currentIndex].currentState!.maybePop();
      },
      child: Scaffold(
        body: IndexedStack(
          index: _currentIndex,
          children: [
            _buildNavigator(0, FeedScreen()),
            _buildNavigator(1, SearchScreen()),
            _buildNavigator(2, ProfileScreen()),
          ],
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: _currentIndex,
          onTap: (index) => setState(() => _currentIndex = index),
          items: [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
            BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
            BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
          ],
        ),
      ),
    );
  }
  
  Widget _buildNavigator(int index, Widget child) {
    return Navigator(
      key: _navigatorKeys[index],
      onGenerateRoute: (settings) {
        return MaterialPageRoute(builder: (_) => child);
      },
    );
  }
}

Use Cases:

  • Tab navigation (keep state per tab)
  • Bottom navigation bar
  • Drawer navigation
  • Master-detail layouts

Navigation Methods

Push (Add to Stack)

dart
// Push new route
Navigator.push(context, MaterialPageRoute(builder: (_) => Screen()));

// Push named route
Navigator.pushNamed(context, '/screen');

Pop (Remove from Stack)

dart
// Go back
Navigator.pop(context);

// Go back with result
Navigator.pop(context, {'success': true});

Push Replacement

Replace current route with new one.

dart
// Replace current route
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => NewScreen()),
);

// Named route
Navigator.pushReplacementNamed(context, '/new');

Use Case: Login → Home (remove login from stack)


Push and Remove Until

Navigate and clear stack until condition.

dart
// Clear all routes and go to home
Navigator.pushNamedAndRemoveUntil(
  context,
  '/home',
  (route) => false,  // Remove all routes
);

// Keep only first route
Navigator.pushNamedAndRemoveUntil(
  context,
  '/success',
  ModalRoute.withName('/'),  // Keep root route
);

Use Case: Logout → Login (clear all screens)


Pop Until

Pop routes until condition is met.

dart
// Pop until home screen
Navigator.popUntil(context, ModalRoute.withName('/home'));

// Pop until first route
Navigator.popUntil(context, (route) => route.isFirst);

Can Pop

Check if there are routes to pop.

dart
if (Navigator.canPop(context)) {
  Navigator.pop(context);
} else {
  // Already at root, maybe exit app
  SystemNavigator.pop();
}

Passing and Returning Data

Forward Data

dart
// Send data forward
final result = await Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => SelectionScreen(options: ['A', 'B', 'C']),
  ),
);

print('Selected: $result');

Return Data

dart
class SelectionScreen extends StatelessWidget {
  final List<String> options;
  
  const SelectionScreen({required this.options});
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: options.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(options[index]),
            onTap: () {
              // Return selected value
              Navigator.pop(context, options[index]);
            },
          );
        },
      ),
    );
  }
}

Custom Transitions

dart
class FadeRoute<T> extends PageRouteBuilder<T> {
  final Widget page;
  
  FadeRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(opacity: animation, child: child);
          },
          transitionDuration: Duration(milliseconds: 500),
        );
}

// Usage
Navigator.push(context, FadeRoute(page: DetailsScreen()));

Deep Linking

Handle external URLs opening specific screens.

dart
MaterialApp(
  onGenerateInitialRoutes: (String initialRoute) {
    // Handle deep links
    if (initialRoute.startsWith('/product/')) {
      final productId = initialRoute.split('/').last;
      return [
        MaterialPageRoute(builder: (_) => HomeScreen()),
        MaterialPageRoute(
          builder: (_) => ProductScreen(id: int.parse(productId)),
        ),
      ];
    }
    return [MaterialPageRoute(builder: (_) => HomeScreen())];
  },
)

Best Practices

Important: Use named routes for maintainability in large apps

✅ Do

dart
// Good - Route constants
class Routes {
  static const home = '/';
  static const profile = '/profile';
  static const settings = '/settings';
}

Navigator.pushNamed(context, Routes.profile);

// Good - Type-safe arguments
class RouteArguments {
  final int id;
  final String name;
  RouteArguments({required this.id, required this.name});
}

// Good - Handle back button
WillPopScope(
  onWillPop: () async {
    // Custom back button logic
    return true;  // Allow pop
  },
  child: Scaffold(...),
)

❌ Don't

dart
// Bad - Magic strings
Navigator.pushNamed(context, '/profile');  // Typo-prone

// Bad - Complex logic in UI
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) {
      // ❌ Complex logic here
      final user = fetchUser();
      return ProfileScreen(user: user);
    },
  ),
);

Resources