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.
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.
Add go_router and uni_links to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
go_router: ^13.0.0
uni_links: ^0.9.0Run flutter pub get:
flutter pub get
Set up your GoRouter with deep link routes:
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(),
);Initialize the router in your MaterialApp:
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,
),
);
}
}Extract and use path parameters in your screens:
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
}
}Access query parameters from the deep link:
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,
);
},
),Protect routes with authentication redirects:
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
],
);Add guards to specific routes using redirect:
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);
},
),Implement deferred deep link handling with initial link detection:
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 deep link handling in your main app:
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,
);
}
}Here's a complete example app with deep linking:
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'),
),
);
}
}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.