Back to Blog
Implementation
10 min read
February 20, 2024

Flutter Deep Linking with go_router

Learn how to implement powerful deep linking in Flutter using go_router. Master route configuration, path parameters, redirect guards, and deferred deep link handling with complete code examples.

Why go_router for Deep Linking?

Key Advantages

  • • Declarative route configuration
  • • Built-in deep link support
  • • Path parameter extraction
  • • Redirect guards for auth
  • • Deep link state tracking
  • • Async parameter parsing

Deep Linking Features

  • • Automatic link parsing
  • • Named route parameters
  • • Query parameter support
  • • Fallback routes
  • • Error handling
  • • State preservation

go_router vs Manual Handling

While uni_links captures deep links, go_router handles the routing logic. Together, they create a complete deep linking solution with declarative route configuration and automatic URL parsing.

Getting Started with go_router

Step 1: Add Dependencies

Add go_router and uni_links to your pubspec.yaml:

yaml
dependencies:
  flutter:
    sdk: flutter
  go_router: ^13.0.0
  uni_links: ^0.9.0

Step 2: Install Packages

Run flutter pub get:

bash
flutter pub get

Configuring Deep Link Routes

Create Router Configuration

Set up your GoRouter with deep link routes:

dart
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';

final GoRouter router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        // Product route with parameter
        GoRoute(
          path: 'product/:productId',
          builder: (context, state) {
            final productId = state.pathParameters['productId']!;
            return ProductScreen(productId: productId);
          },
        ),
        // User profile route
        GoRoute(
          path: 'user/:userId',
          builder: (context, state) {
            final userId = state.pathParameters['userId']!;
            return UserProfileScreen(userId: userId);
          },
        ),
        // Referral/share route
        GoRoute(
          path: 'share/:referralCode',
          builder: (context, state) {
            final referralCode = state.pathParameters['referralCode']!;
            return ShareScreen(referralCode: referralCode);
          },
        ),
      ],
    ),
    // 404 not found route
    GoRoute(
      path: '/404',
      builder: (context, state) => const NotFoundScreen(),
    ),
  ],
  // Handle unmatched routes
  errorBuilder: (context, state) => const NotFoundScreen(),
);

Use Router in Main App

Initialize the router in your MaterialApp:

dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'My App',
      routerConfig: router,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

Path Parameter Extraction

Accessing Path Parameters

Extract and use path parameters in your screens:

dart
class ProductScreen extends StatelessWidget {
  final String productId;

  const ProductScreen({Key? key, required this.productId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Product')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Product ID: $productId'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Load product details
                _loadProductDetails(productId);
              },
              child: const Text('Load Product'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _loadProductDetails(String productId) async {
    debugPrint('Loading product: $productId');
    // Fetch from API using productId
  }
}

Query Parameters

Access query parameters from the deep link:

dart
GoRoute(
  path: 'product/:productId',
  builder: (context, state) {
    final productId = state.pathParameters['productId']!;
    // Access query parameters
    final String? source = state.uri.queryParameters['source'];
    final String? campaign = state.uri.queryParameters['campaign'];

    return ProductScreen(
      productId: productId,
      source: source,
      campaign: campaign,
    );
  },
),

Redirect Guards & Auth Checks

Implement Auth Guard

Protect routes with authentication redirects:

dart
final GoRouter router = GoRouter(
  redirect: (context, state) {
    // Check if user is authenticated
    final isAuthenticated = _isUserAuthenticated();
    final isLoggingIn = state.uri.path == '/login';

    if (!isAuthenticated && !isLoggingIn) {
      // Redirect to login, but save the requested path
      return '/login?from=${state.uri.path}';
    }

    if (isAuthenticated && isLoggingIn) {
      // Already logged in, redirect to home
      return '/';
    }

    return null; // No redirect needed
  },
  routes: [
    // Your routes here
  ],
);

Route-Level Guards

Add guards to specific routes using redirect:

dart
GoRoute(
  path: 'user/:userId',
  redirect: (context, state) {
    // Check if user has permission to view
    final userId = state.pathParameters['userId']!;
    final currentUserId = _getCurrentUserId();

    if (userId != currentUserId && !_isAdmin()) {
      return '/'; // Redirect to home if no permission
    }

    return null; // Allow access
  },
  builder: (context, state) {
    final userId = state.pathParameters['userId']!;
    return UserProfileScreen(userId: userId);
  },
),

Deferred Deep Link Handling

Handle Deferred Links with go_router

Implement deferred deep link handling with initial link detection:

dart
import 'package:uni_links/uni_links.dart';

class DeepLinkManager {
  static Future<void> initializeDeepLinks(GoRouter router) async {
    // Handle initial deep link (app launched from deep link)
    final String? initialLink = await getInitialLink();
    if (initialLink != null) {
      await _processDeepLink(initialLink, router);
    }

    // Handle deep links while app is running
    linkStream.listen(
      (String link) {
        _processDeepLink(link, router);
      },
      onError: (err) {
        debugPrint('Deep link error: $err');
      },
    );
  }

  static Future<void> _processDeepLink(
    String link,
    GoRouter router,
  ) async {
    final Uri uri = Uri.parse(link);

    try {
      // For Universal Links/App Links, use go_router's built-in handling
      if (uri.scheme == 'https') {
        // Construct the path from the URI
        final String path = uri.path;
        final String? queryString = uri.query.isNotEmpty ? '?${uri.query}' : '';

        // Use go_router to navigate
        router.go('$path$queryString');
      } else if (uri.scheme == 'myapp') {
        // Custom scheme handling
        router.go(uri.path, extra: uri.queryParameters);
      }
    } catch (e) {
      debugPrint('Error processing deep link: $e');
    }
  }
}

Initialize in Main App

Initialize deep link handling in your main app:

dart
class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final GoRouter _router;

  @override
  void initState() {
    super.initState();
    _router = _createRouter();

    // Initialize deep link handling after router is created
    WidgetsBinding.instance.addPostFrameCallback((_) {
      DeepLinkManager.initializeDeepLinks(_router);
    });
  }

  GoRouter _createRouter() {
    return GoRouter(
      routes: [
        // Your routes
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'My App',
      routerConfig: _router,
    );
  }
}

Full Working Example

Complete App Implementation

Here's a complete example app with deep linking:

dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final GoRouter _router = _createRouter();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _initializeDeepLinks();
    });
  }

  GoRouter _createRouter() {
    return GoRouter(
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
          routes: [
            GoRoute(
              path: 'product/:productId',
              builder: (context, state) {
                final productId = state.pathParameters['productId']!;
                return ProductScreen(productId: productId);
              },
            ),
            GoRoute(
              path: 'user/:userId',
              builder: (context, state) {
                final userId = state.pathParameters['userId']!;
                return UserProfileScreen(userId: userId);
              },
            ),
          ],
        ),
      ],
      errorBuilder: (context, state) => const NotFoundScreen(),
    );
  }

  Future<void> _initializeDeepLinks() async {
    final String? initialLink = await getInitialLink();
    if (initialLink != null) {
      _router.go(_parseDeepLink(initialLink));
    }

    linkStream.listen((String link) {
      _router.go(_parseDeepLink(link));
    });
  }

  String _parseDeepLink(String link) {
    final uri = Uri.parse(link);
    return uri.path;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Deep Link App',
      routerConfig: _router,
    );
  }
}

// Screens
class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: const Center(child: Text('Home Screen')),
    );
  }
}

class ProductScreen extends StatelessWidget {
  final String productId;

  const ProductScreen({Key? key, required this.productId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Product')),
      body: Center(
        child: Text('Product ID: $productId'),
      ),
    );
  }
}

class UserProfileScreen extends StatelessWidget {
  final String userId;

  const UserProfileScreen({Key? key, required this.userId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: Center(
        child: Text('User ID: $userId'),
      ),
    );
  }
}

class NotFoundScreen extends StatelessWidget {
  const NotFoundScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Not Found')),
      body: const Center(
        child: Text('Page not found'),
      ),
    );
  }
}

Best Practices

Route Organization

  • • Keep route definitions modular
  • • Extract routes into separate files
  • • Use descriptive path names
  • • Group related routes together
  • • Document route parameters
  • • Version routes if needed

Error Handling

  • • Always provide error routes
  • • Log invalid deep links
  • • Provide fallback navigation
  • • Validate parameters
  • • Handle null safely
  • • Test edge cases
Pro Tips
  • • Use RouteMatch to debug route parsing
  • • Test deep links with adb on Android and xcode-select on iOS
  • • Preserve state during navigation with keepAlive
  • • Use query parameters for optional data
  • • Combine with Riverpod or GetX for state management

Master Complete Deep Linking

You now have the knowledge to implement complete deep linking with go_router. Learn about deferred deep linking for advanced attribution and app install tracking.

Related Articles