---
title: "Navigation and Linking"
description: "The Navigator and the route stack, named routes, passing data, transitions, and deep versus dynamic links."
tags: [flutter, dart, navigation, deep-linking]
---

## Navigation

Navigation in Flutter is a stack of routes managed by a `Navigator` widget. Named routes, arguments, and custom transitions are all built on that one idea.

### Navigator and Route

At the core of Flutter's navigation system are two key elements:

1. **Navigator**: A widget that manages a stack of `Route` objects.
2. **Route**: An abstraction representing a "screen" or "page" in your app.

#### The Navigator widget

The `Navigator` is actually a stateful widget that maintains a stack data structure of routes. This stack follows the LIFO (Last In, First Out) principle, which means:

* The last route added to the stack is the first one removed
* The topmost route in the stack is what the user sees on screen

The `Navigator` widget is usually created for you automatically when you use `MaterialApp` or `CupertinoApp`, and it's accessible through a static method:

```dart
Navigator.of(context)
```

Or through a shorthand:

```dart
Navigator.push(context, route)
```

#### The Route class

A `Route` is an abstract class in Flutter that represents a screen. There are several implementations:

* **MaterialPageRoute**: Provides a platform-adaptive transition. On Android, it slides in from the bottom, while on iOS, it slides in from the right.
* **CupertinoPageRoute**: Provides iOS-style transitions.
* **PageRouteBuilder**: Allows you to create custom route transitions.

### The navigation stack

Flutter's navigation implements a stack-based system, where routes are pushed and popped from a stack maintained by the Navigator.

#### How stack operations work

1. **Pushing a Route**:

   * When `Navigator.push()` is called, a new route is created and added to the top of the stack
   * The Navigator displays the new route with an animation
   * The previous route is still in memory but not visible (it's "underneath" the new route)
2. **Popping a Route**:

   * When `Navigator.pop()` is called, the topmost route is removed from the stack
   * The Navigator displays the transition animation as the current route is removed
   * The previous route (now at the top of the stack) becomes visible again

Here's what happens internally when `Navigator.push()` is called:

1. The Navigator widget receives the push request
2. It calls `createRoute()` on the provided Route object to create the actual route
3. It adds this route to its internal routes list
4. It updates its state, which triggers a rebuild
5. During the rebuild, it renders the new route with an animation

### The role of BuildContext

What `BuildContext` is in general is covered in [widgets-state-and-lifecycle](https://brain.narayann.dev/notes/engineering/flutter/widgets-state-and-lifecycle). Navigation cares about
one property of it: the context you pass to `Navigator.of(context)` decides which navigator you get.

`BuildContext` is crucial for navigation. It represents the location of a widget in the widget tree and provides access to the nearest `Navigator` ancestor.

When you call `Navigator.of(context)`, Flutter traverses up the widget tree starting from the given context to find the nearest `Navigator` widget. This is why you need a valid `BuildContext` to perform navigation.

### Named routes vs direct navigation

#### Direct navigation (imperative)

When using direct navigation with `Navigator.push()`, you create a new route instance and provide it directly to the Navigator:

```dart
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);
```

Internally, this:

1. Creates a new `MaterialPageRoute` with a builder function
2. When the route is pushed, the builder function is called to create the actual widget
3. The route handles the animation and rendering of the new screen

#### Named routes (declarative)

With named routes, you pre-define your routes in the `MaterialApp`:

```dart
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/details': (context) => DetailScreen(),
  },
)
```

When you call `Navigator.pushNamed(context, '/details')`, Flutter:

1. Looks up the route name in the predefined routes map
2. Creates a `MaterialPageRoute` with the builder function from the map
3. Pushes this route onto the Navigator's stack

### `onGenerateRoute`

For more dynamic routing, Flutter provides `onGenerateRoute`, which gets called when you navigate to a named route that isn't defined in the `routes` map:

```dart
MaterialApp(
  onGenerateRoute: (settings) {
    // Extract route name
    final name = settings.name;
    // Extract route arguments
    final arguments = settings.arguments;

    // Parse and handle the route
    if (name == '/product') {
      return MaterialPageRoute(
        builder: (context) => ProductScreen(id: arguments),
      );
    }
    // Return null to allow Flutter to handle routes it knows about
    return null;
  },
)
```

This function receives a `RouteSettings` object containing:

* `name`: The route name that was requested
* `arguments`: Any arguments passed to the route

You can then conditionally create and return a `Route` object based on the requested route name and arguments.

### Passing and returning data

#### Constructor parameters

The simplest approach is passing data via the constructor:

```dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(product: product),
  ),
);
```

Internally, this works because:

1. When the route is created, it captures the reference to your data object
2. When the route is activated, it creates the destination widget with the data

#### Named routes with arguments

When using named routes, you can pass data through the `arguments` parameter:

```dart
Navigator.pushNamed(
  context,
  '/details',
  arguments: {'id': 123, 'name': 'Product'},
);
```

To retrieve this data:

```dart
final arguments = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
```

How this works:

1. The arguments are stored in the `RouteSettings` object
2. `ModalRoute.of(context)` gets the current route for the given context
3. From that route, you can access the settings and arguments

#### Returning data from a route

When a screen needs to return data:

```dart
// In the calling screen
final result = await Navigator.push(...);

// In the called screen
Navigator.pop(context, resultData);
```

Internally:

1. `Navigator.push()` returns a `Future` that will complete when the pushed route is popped
2. When `Navigator.pop(context, data)` is called, it completes the Future with the provided data
3. The `await` in the calling code receives this data when the navigation completes

### Transition animations

Flutter's route transitions are implemented using animation controllers. When a route is pushed or popped:

1. An `AnimationController` is created with the specified duration
2. The controller drives a set of animations (like position, opacity, or scale)
3. These animations update the visual representation of the route on each frame

For custom transitions using `PageRouteBuilder`:

```dart
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // Use animation to build a custom transition
      return FadeTransition(opacity: animation, child: child);
    },
  ),
);
```

Here's what happens:

1. The `pageBuilder` creates the actual page widget
2. The `transitionsBuilder` uses the provided animations to create a transition effect
3. The primary `animation` goes from 0.0 to 1.0 during a push, and from 1.0 to 0.0 during a pop
4. The `secondaryAnimation` represents the animation of the route underneath

Let's tie it all together with a complete example of what happens when you navigate:

1. **Initial Setup**:

   * The app has a `Navigator` widget (typically from `MaterialApp`)
   * The Navigator maintains a stack of routes (initially just the home route)
2. **When `Navigator.push()` is called**:

   * A new route is created
   * The Navigator adds it to the top of the stack
   * The Navigator starts the "push" animation
   * The previous route's `didPushNext` is called
   * The new route's `didPush` is called
   * The screen is redrawn with the new route visible
3. **When `Navigator.pop()` is called**:

   * The Navigator starts the "pop" animation
   * The current route's `didPop` is called
   * The previous route's `didPopNext` is called
   * Once the animation completes, the top route is removed from the stack
   * The screen is redrawn with the previous route visible
4. **When `Navigator.pushReplacement()` is called**:

   * A new route is created
   * The Navigator starts the "push" animation
   * Once the animation completes, the top route is replaced with the new one
   * The screen is redrawn with the new route visible

Understanding this underlying navigation mechanism gives you full control over your app's navigation flow and allows you to implement complex navigation patterns effectively.

### Nested navigation

Flutter also supports nested navigation, where you can have multiple navigators in the widget tree. This is useful for implementing bottom navigation bars where each tab has its own navigation stack.

In this case:

1. Each tab has its own `Navigator` widget with its own navigation stack
2. When switching tabs, you're actually switching between different Navigator widgets
3. Each Navigator maintains its state independently

This creates a more complex but powerful navigation structure that can handle sophisticated app flows while maintaining good performance.

***

## Deep links vs dynamic links

Both open your app on a specific screen from outside the app. A deep link stops at the store if the app is not installed. A dynamic link survives the install and takes the user to the right screen afterwards.

### Deep links

Deep linking refers to the ability to use a URL or URI to navigate users directly to a specific screen or content within a mobile application, rather than just opening the app's home screen.

#### Characteristics

1. **Direct Navigation**: Takes users directly to specific content within an app
2. **URL/URI Based**: Uses a URL or URI scheme to define the destination
3. **App Installation Requirement**: Traditional deep links work only if the app is already installed

#### Types of deep link

1. **Custom URL Schemes**: Using a custom protocol (e.g., `myapp://products/123`)
2. **Universal Links (iOS)**: Standard HTTP/HTTPS URLs that can open specific content in apps
3. **App Links (Android)**: Android's version of Universal Links

#### Implementation in Flutter

```dart
// Handle incoming links
void initDeepLinks() {
  // For already running app
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) {
      handleDeepLink(uri);
    }
  });

  // For app start
  getInitialUri().then((Uri? uri) {
    if (uri != null) {
      handleDeepLink(uri);
    }
  });
}

void handleDeepLink(Uri uri) {
  // Parse the URI and navigate accordingly
  if (uri.pathSegments.contains('products')) {
    final productId = uri.pathSegments.last;
    Navigator.pushNamed(context, '/product', arguments: productId);
  }
}
```

### Dynamic links

Dynamic linking is deep linking plus deferred behaviour: the link keeps working when the app is not installed yet, sending the user to the store and then to the right screen after the install.

Firebase Dynamic Links was the usual way to do this and **shut down on 25 August 2025**. Existing
links now return 404 and `.page.link` domains are gone. The code below is kept because the concepts
carry over, but for new work use App Links on Android and Universal Links on iOS for the deep link
half, and an attribution provider such as Branch, AppsFlyer, or Adjust when you need the deferred
half.

#### Characteristics

1. **Cross-Platform Support**: Works across iOS and Android
2. **Persistence**: Links work even if the app isn't installed yet
3. **App Store Redirection**: Can redirect users to app stores if the app isn't installed
4. **Contextual Information**: Can carry additional parameters and analytics data
5. **Deferred Deep Linking**: Can navigate to the intended content after app installation

#### Key features

1. **Smart Link Redirection**: Automatically directs users to the proper destination based on platform and app installation status
2. **Conversion Tracking**: Provides analytics on link usage and conversions
3. **UTM Parameters Support**: Enables marketing campaign tracking
4. **Shortened URLs**: Creates more user-friendly links

#### Implementation with Firebase Dynamic Links (retired)

```dart
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';

Future<void> initDynamicLinks() async {
  // Handle links when app is started by a link
  final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
  if (initialLink != null) {
    handleDynamicLink(initialLink);
  }

  // Handle links when app is already running
  FirebaseDynamicLinks.instance.onLink.listen(
    (dynamicLinkData) {
      handleDynamicLink(dynamicLinkData);
    },
    onError: (e) => print('Dynamic Link error: ${e.message}')
  );
}

void handleDynamicLink(PendingDynamicLinkData data) {
  final Uri deepLink = data.link;

  // Example link: https://example.com/products/123?source=marketing
  if (deepLink.pathSegments.contains('products')) {
    final productId = deepLink.pathSegments.last;
    final source = deepLink.queryParameters['source'];

    // Navigate and track source
    Navigator.pushNamed(
      context,
      '/product',
      arguments: {'id': productId, 'source': source}
    );
  }
}

// Creating a dynamic link
Future<Uri> createDynamicLink(String productId) async {
  final DynamicLinkParameters parameters = DynamicLinkParameters(
    uriPrefix: 'https://yourapp.page.link',
    link: Uri.parse('https://yourapp.com/products/$productId'),
    androidParameters: AndroidParameters(
      packageName: 'com.yourcompany.yourapp',
      minimumVersion: 1,
    ),
    iosParameters: IosParameters(
      bundleId: 'com.yourcompany.yourapp',
      minimumVersion: '1.0.0',
      appStoreId: '123456789',
    ),
    socialMetaTagParameters: SocialMetaTagParameters(
      title: 'View Product',
      description: 'Check out this amazing product!',
      imageUrl: Uri.parse('https://yourapp.com/images/product.jpg'),
    ),
  );

  final shortDynamicLink = await FirebaseDynamicLinks.instance.buildShortLink(parameters);
  return shortDynamicLink.shortUrl;
}
```

### Side by side

| Feature                      | Deep Linking                     | Dynamic Linking                              |
| ---------------------------- | -------------------------------- | -------------------------------------------- |
| App Installation Requirement | Requires app to be installed     | Works with or without app installation       |
| Cross-Platform Support       | Requires separate implementation | Single solution for multiple platforms       |
| Store Redirection            | No built-in redirection          | Automatic app store redirection              |
| Analytics                    | No built-in analytics            | Integrated analytics and attribution         |
| Link Lifespan                | Permanent                        | Can be configured (short-lived or permanent) |
| Implementation Complexity    | Lower                            | Higher                                       |
| Backend Requirements         | Minimal                          | Requires service like Firebase               |

### When each is used

Dynamic linking could be particularly valuable for:

1. **User Onboarding**: Sending personalized links that direct new users to a specific onboarding flow
2. **Referral Programs**: Creating shareable links that track who referred a new user
3. **Transaction Sharing**: Letting users share transaction details with others
4. **Marketing Campaigns**: Tracking which campaigns drive the most app installations and conversions
5. **Customer Support**: Directing users to specific sections based on support queries
