A deep link may arrive while the app is running, starting from a terminated state, or waiting on authentication. It may point to content that moved or that the current user cannot access.
Parsing the URL is only the first step. The real work is turning an external intention into valid application navigation.
Convert external URLs into typed destinations#
Do not pass raw URLs through widgets:
sealed class AppDestination {
const AppDestination();
}
final class CourseDestination extends AppDestination {
const CourseDestination(this.courseId);
final String courseId;
}
final class ProfileDestination extends AppDestination {
const ProfileDestination();
}
Parse links at one boundary:
AppDestination? parseDestination(Uri uri) {
final segments = uri.pathSegments;
if (segments.length == 2 && segments.first == 'courses') {
return CourseDestination(segments[1]);
}
if (segments.length == 1 && segments.first == 'profile') {
return const ProfileDestination();
}
return null;
}
This keeps provider-specific query parameters and legacy URL shapes out of the router.
Hold the destination while authentication completes#
Redirecting an unauthenticated user to login is only half the behavior. After login, they should continue to the original destination.
class PendingNavigation {
AppDestination? value;
AppDestination? take() {
final destination = value;
value = null;
return destination;
}
}
When the link arrives, store the typed destination if authentication is required. After successful login and startup initialization, consume it once.
Avoid storing a BuildContext or NavigatorState. Store data describing the destination.
Let the app router own the final mapping#
go_router can represent nested route trees, redirects, and shell navigation:
final router = GoRouter(
routes: [
GoRoute(
path: '/courses/:id',
builder: (context, state) {
return CoursePage(
courseId: state.pathParameters['id']!,
);
},
),
],
);
A shared feature package can expose CoursePage. The application should decide its public path, authentication redirect, shell placement, and fallback behavior.
Do not make a reusable card know that the app uses /courses/:id. Give it an onOpen callback or a navigation contract.
Handle every app lifecycle#
Test three paths:
- Terminated: startup receives the initial link before the session and router are ready.
- Background: tapping a notification resumes the app with a destination.
- Foreground: the running app receives a link stream event.
Send all three through one coordinator. Otherwise initial links and foreground links slowly develop different behavior.
Guard against processing the same link twice when both the platform and a link provider report it.
Resolve content before opening complex routes#
A link may contain an ID but not the data a page needs. Let the destination page load by ID, or introduce a resolver:
Future<void> openCourse(
String id,
AppNavigator navigator,
) async {
final course = await courses.getById(id);
if (course == null) {
navigator.showUnavailableContent();
return;
}
navigator.openCourse(course.id);
}
Decide what happens when content is deleted, access is denied, the user is offline, or an old URL format arrives. A blank page is not a fallback strategy.
Separate link providers from destinations#
A service such as Branch may add attribution, campaign parameters, and provider-specific payloads. Map those into the same AppDestination used by universal links and notification taps.
Branch payload ─┐
Universal link ─┼─▶ Destination parser ─▶ Navigation coordinator
FCM data ───────┘
This prevents three separate navigation systems.
Track outcomes, not only taps#
Useful analytics distinguish:
- link received;
- link rejected as invalid;
- authentication required;
- destination opened;
- content unavailable;
- access denied.
Do not include sensitive query values in analytics. Use destination type and safe campaign identifiers.
Deep links became reliable for me after treating them as deferred navigation state. A URL is an input. Authentication, startup, content resolution, and the app-owned route graph determine whether that intention can actually be completed.