BLoC Deep Link Navigation: Handling App Links & Universal Links
Last Sync: Today
flutter
BLoC Deep Link Navigation: Handling App Links & Universal Links
Introduction
Deep links (also called app links or universal links) allow users to navigate directly to specific content within your app from a URL. When combined with BLoC, you can manage the navigation flow, parse link parameters, and update state accordingly. This guide covers how to set up deep link handling using go_router and BLoC, process incoming links, and maintain state across navigation.
Why Use BLoC for Deep Links?
Centralized Logic – Handle deep link parsing and navigation in one place (BLoC).
State Management – Keep track of deep link data (e.g., user ID, product ID) as part of app state.
Testing – Isolate deep link handling logic for unit tests.
Reactive Updates – Automatically trigger navigation or UI updates when a deep link is received.
Platform Abstraction – Unify iOS (universal links) and Android (app links) handling under a single BLoC interface.
Setting Up Deep Links
First, configure your app to receive deep links. This involves setting up associated domains (iOS), intent filters (Android), and linking your app with a hosted asset file. This guide assumes you have already configured platform‑specific deep link support. For Flutter, we'll use the go_router package which has built‑in deep link handling.
Create a BLoC to manage deep link events. The state holds the current deep link (if any) and whether it has been processed.
DARTRead-only
1
// deep_link_state.dart
part of'deep_link_bloc.dart';
@immutable
abstract classDeepLinkStateextendsEquatable{constDeepLinkState();}classDeepLinkInitialextendsDeepLinkState{constDeepLinkInitial();
@override
List<Object?>getprops=>[];}classDeepLinkReceivedextendsDeepLinkState{
final Uri uri;
final Map<String, String> params;constDeepLinkReceived(this.uri,this.params);
@override
List<Object?>getprops=>[uri, params];}classDeepLinkProcessedextendsDeepLinkState{constDeepLinkProcessed();
@override
List<Object?>getprops=>[];}// deep_link_event.dart
abstract classDeepLinkEventextendsEquatable{constDeepLinkEvent();}classDeepLinkOpenedextendsDeepLinkEvent{
final Uri uri;constDeepLinkOpened(this.uri);
@override
List<Object?>getprops=>[uri];}classDeepLinkHandledextendsDeepLinkEvent{constDeepLinkHandled();
@override
List<Object?>getprops=>[];}
Deep Link BLoC Implementation
DARTRead-only
1
classDeepLinkBlocextendsBloc<DeepLinkEvent, DeepLinkState>{DeepLinkBloc():super(constDeepLinkInitial()){
on<DeepLinkOpened>(_onDeepLinkOpened);
on<DeepLinkHandled>(_onDeepLinkHandled);}
Future<void>_onDeepLinkOpened(DeepLinkOpened event, Emitter<DeepLinkState> emit) async {// Parse URI parameters (example: /product/123?referrer=email)
final pathSegments = event.uri.pathSegments;
final queryParams = event.uri.queryParameters;
final params =<String, String>{};// Example: parse product ID from pathif(pathSegments.isNotEmpty && pathSegments.first =='product'&& pathSegments.length >1){
params['productId']= pathSegments[1];}// Add all query parameters
params.addAll(queryParams);emit(DeepLinkReceived(event.uri, params));}
Future<void>_onDeepLinkHandled(DeepLinkHandled event, Emitter<DeepLinkState> emit) async {emit(constDeepLinkProcessed());}}
Integrating go_router with Deep Links
go_router can handle deep links by defining a redirect function that intercepts incoming routes and decides whether to navigate to a specific page based on the deep link data.
DARTRead-only
1
final router =GoRouter(initialLocation:'/',routes:[GoRoute(path:'/',name:'home',builder:(context, state)=>HomePage(),),GoRoute(path:'/product/:id',name:'product',builder:(context, state)=>ProductPage(productId: state.params['id']!),),GoRoute(path:'/profile/:userId',name:'profile',builder:(context, state)=>ProfilePage(userId: state.params['userId']!),),],redirect:(context, state){// Example: check if deep link is pending in BLoC
final deepLinkState = context.read<DeepLinkBloc>().state;if(deepLinkState is DeepLinkReceived){
final uri = deepLinkState.uri;// Determine where to navigate based on the URIif(uri.pathSegments.isNotEmpty){if(uri.pathSegments.first =='product'){
final productId = uri.pathSegments[1];return'/product/$productId';}elseif(uri.pathSegments.first =='profile'){
final userId = uri.pathSegments[1];return'/profile/$userId';}}// If no matching route, fallback to homereturn'/';}returnnull;// no redirect},);
Handling Deep Links at App Start
When the app is launched from a deep link, the initial URI is available via WidgetsBinding.instance.window.defaultRouteName or through go_router's initial location. You should read this initial link and add the DeepLinkOpened event to the BLoC.
DARTRead-only
1
voidmain() async {
WidgetsFlutterBinding.ensureInitialized();// Parse initial deep link if any
final initialUri =_getInitialUri();
final deepLinkBloc =DeepLinkBloc();if(initialUri !=null){
deepLinkBloc.add(DeepLinkOpened(initialUri));}runApp(MyApp(deepLinkBloc: deepLinkBloc));}
Uri?_getInitialUri(){// For platforms where initial route is a deep link
final initialRoute = WidgetsBinding.instance.window.defaultRouteName;if(initialRoute.startsWith('http://')|| initialRoute.startsWith('https://')){return Uri.parse(initialRoute);}returnnull;}classMyAppextendsStatelessWidget{
final DeepLinkBloc deepLinkBloc;constMyApp({required this.deepLinkBloc});
@override
Widget build(BuildContext context){returnBlocProvider(create:(_)=> deepLinkBloc,child: MaterialApp.router(routerConfig: router,),);}}
Listening to Deep Links While App is Running
For deep links received when the app is already open, you need to listen to platform events. Use app_links or uni_links package. Then add a listener that adds the DeepLinkOpened event to your BLoC.
DARTRead-only
1
import'package:app_links/app_links.dart';classDeepLinkService{
final DeepLinkBloc deepLinkBloc;
final AppLinks _appLinks =AppLinks();DeepLinkService(this.deepLinkBloc){_init();}void_init() async {// Handle initial link (if any) – already handled in main// Listen to future links
_appLinks.uriLinkStream.listen((Uri? uri){if(uri !=null){
deepLinkBloc.add(DeepLinkOpened(uri));}});}}// In main or a service locator
final deepLinkBloc =DeepLinkBloc();
final deepLinkService =DeepLinkService(deepLinkBloc);
Using Deep Link Data in Pages
When a deep link leads to a specific page, you can use the BLoC state to retrieve any additional parameters or handle special logic. For example, after the deep link is processed, you might want to show a snackbar or perform some action.
DARTRead-only
1
classProductPageextendsStatelessWidget{
final String productId;constProductPage({required this.productId});
@override
Widget build(BuildContext context){// Optionally, you can listen to deep link events to show a bannerreturn BlocListener<DeepLinkBloc, DeepLinkState>(listenWhen:(previous, current)=>
current is DeepLinkReceived &&
current.params['productId']== productId &&
previous is! DeepLinkReceived,listener:(context, state){if(state is DeepLinkReceived){// Show a message that the user arrived via deep link
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text('Opened via deep link from ${state.params['referrer'] ?? 'unknown'}')),);// Mark as handled so it doesn't show again
context.read<DeepLinkBloc>().add(DeepLinkHandled());}},child:Scaffold(appBar:AppBar(title:Text('Product $productId')),body:Center(child:Text('Product details')),),);}}
Testing Deep Link Handling
You can test deep link logic by dispatching events to the BLoC and verifying state changes and navigation. Use blocTest to simulate deep link events and check expected states.
DARTRead-only
1
voidmain(){group('DeepLinkBloc',(){
blocTest<DeepLinkBloc, DeepLinkState>('emits DeepLinkReceived when product link opened',build:()=>DeepLinkBloc(),act:(bloc)=> bloc.add(DeepLinkOpened(Uri.parse('https://example.com/product/123?ref=email'))),expect:()=>[DeepLinkReceived(
Uri.parse('https://example.com/product/123?ref=email'),{'productId':'123','ref':'email'},),],);
blocTest<DeepLinkBloc, DeepLinkState>('emits DeepLinkProcessed after handled',build:()=>DeepLinkBloc(),seed:()=>DeepLinkReceived(Uri.parse('https://example.com/product/123'),{'productId':'123'}),act:(bloc)=> bloc.add(DeepLinkHandled()),expect:()=>[constDeepLinkProcessed()],);});}
Best Practices
Centralize deep link parsing – Use a BLoC or service to parse URI and extract parameters.
Handle initial and incoming links – Cover both app start and runtime deep links.
Use go_router for navigation – It handles deep link matching and state management well.
Keep deep link state in BLoC – This allows other parts of the app to react to deep link arrival.
Mark deep links as handled – Avoid repeated processing (e.g., showing snackbar multiple times).
Test both BLoC and router integration – Ensure deep links lead to the correct screens.
Consider error handling – If a deep link is malformed, navigate to a default screen.
Common Mistakes
❌ Not handling both startup and runtime links – Deep links may be missed.
✅ Listen to both initial and subsequent links.
❌ Performing navigation directly inside BLoC – Should be handled by the router or a navigation service.
✅ Use go_router's redirect or push events to BLoC that trigger navigation.
❌ Parsing URIs without validation – Malformed links can crash the app.
✅ Validate and provide fallback.
❌ Ignoring platform-specific configuration – Android and iOS require specific setup for deep links to work.
✅ Follow platform guides for associated domains and intent filters.
❌ Not disposing BLoC listeners – Can cause memory leaks.
✅ Close subscriptions in close() or use BlocListener properly.
Conclusion
Deep links are a powerful way to drive user engagement. By combining BLoC with a routing solution like go_router, you can centralize deep link handling, parse parameters, and navigate seamlessly. The BLoC pattern provides a reactive, testable foundation for managing deep link state across your application.
What package is commonly used to listen to incoming deep links in Flutter?
A
app_links
B
uni_links
C
url_launcher
D
Both A and B
Q2
of 3
In go_router, where do you place logic to handle deep link routing?
A
In the builder of each route
B
In the redirect function
C
In the initialLocation
D
In a separate BlocListener
Q3
of 3
Why should you use a BLoC for deep link handling?
A
It replaces the need for a router
B
It centralizes parsing and state management
C
It improves performance of deep links
D
It is required by the platform
Frequently Asked Questions
What packages are needed for deep links in Flutter?
For deep link support, you'll need go_router (or auto_route) for routing and app_links or uni_links to listen to incoming links. url_launcher is optional for testing.
How do I test deep links in development?
On Android, use adb shell am start -a android.intent.action.VIEW -d 'https://yourdomain.com/product/123'. On iOS, use xcrun simctl openurl booted 'https://yourdomain.com/product/123'. In Flutter, you can simulate by adding the event to the BLoC directly in tests.
Can I use deep links with `go_router` without BLoC?
Yes, go_router has built‑in deep link support via redirect and initialLocation. However, BLoC can help manage additional data and coordinate side effects.
How do I handle deep links that require authentication?
In the redirect function, check authentication state (from your auth BLoC). If not authenticated, redirect to login and store the deep link to navigate after login.
What's the difference between `app_links` and `uni_links`?
Both handle incoming links. app_links is more actively maintained and supports both initial and future links in a simpler API.