Navigation & Deep Linking Done Right: GoRouter, App Links, and Universal Links
Stop relying on `Navigator.push()` for everything. Learn to orchestrate declarative routing, handle auth gating safely, and debug platform-level deep links like a senior engineer.
01. Navigator 1.0 vs 2.0 (The Truth)
In interviews, you will inevitably be asked to explain the difference between Navigator 1.0 and 2.0. Keep it precise and brief.
Navigator 1.0 is imperative. You tell the framework exactly what to do: `Navigator.push(context, route)`. The framework treats the screen stack as an array it blindly mutates. It has no concept of a "URL" mapping to the current state.
Navigator 2.0 is declarative and URL-driven. The app state determines what should be on the navigation stack. It relies on a trio of classes: a `Router` widget, a `RouteInformationParser` (translates URLs to state), and a `RouterDelegate` (builds the UI based on state). When a deep link arrives from the OS, the framework updates the state, and the UI reacts.
Here is the honest truth you should say out loud in an interview: Raw Navigator 2.0 is so incredibly verbose that almost nobody hand-rolls those three classes directly in production anymore. We rely on packages built on top of that API.
02. GoRouter: The De Facto Standard
The current industry standard is GoRouter. It is maintained by the Flutter team and provides an ergonomic API over the underlying Navigator 2.0 declarative model.
When an interviewer asks why you chose GoRouter for a project, they aren't looking for "because Google makes it." They want to hear about these specific architectural problems it solves:
- Type-Safe Route Generation: With `go_router_builder`, you define routes as annotated classes. You stop pushing stringly-typed routes like `context.push('/product/$id')` and start calling `ProductRoute(id: id).go(context)`. The compiler guarantees you don't miss a required parameter.
- Nested / Shell Routes: Think of an app with a persistent Bottom Navigation Bar. If you use a naive imperative approach, switching from the "Home" tab to the "Cart" tab destroys the "Home" tab's internal navigation stack. GoRouter's `StatefulShellRoute` maintains independent navigation stacks for each tab, surviving tab switches automatically.
- Centralized Redirect Logic: GoRouter allows you to define a single `redirect` callback that intercepts every navigation event. This is how you implement robust auth gating without polluting individual UI screens.
03. The Trade-off: AutoRoute
Not every top product company uses GoRouter. Many senior teams use AutoRoute. If an interviewer asks you to compare them, frame it precisely as a trade-off between compile-time safety and setup ceremony.
- Maintained by Flutter team (officially blessed).
- Lighter weight; can be used entirely without code generation if desired.
- Redirect logic is centralized in one massive function (can get unwieldy in massive apps).
- Type-safe codegen is available via `go_router_builder`, but feels bolted on.
- Heavily reliant on `build_runner` (higher codegen ceremony).
- Stronger, more mature compile-time route generation and argument passing out-of-the-box.
- Uses Route Guards (`AutoRouteGuard`) allowing logic to be split per-route rather than centralized.
- Often preferred in massive enterprise monoliths.
04. Deep Linking Mechanics (Android & iOS)
Let's stick to our consistent example: A Shopping App. Marketing sends out a push notification with a link: `https://shop.example.com/item/123`.
For that link to open your app natively instead of the web browser, the OS must verify that the app is authorized to handle that specific domain.
- Android App Links: Require an `assetlinks.json` file hosted at `https://shop.example.com/.well-known/assetlinks.json`. It contains your app's package name and SHA256 signing certificate fingerprint.
- iOS Universal Links: Require an `apple-app-site-association` (AASA) file hosted at `https://shop.example.com/.well-known/apple-app-site-association`. It contains your App ID (Team ID + Bundle ID).
Tap to reveal the diagnostic steps a senior engineer takes.
05. State Restoration: The "Senior" Auth Gate
Here is where candidates fail the interview. A user taps a deep link to `shop.example.com/item/123`. The app opens. GoRouter parses the URL. However, the user is not logged in, and viewing a product requires an active session in our app.
The Junior approach: The auth redirect intercepts the route, sends the user to `/login`. The user logs in. The app routes them to `/home`. The original intent (seeing item 123) is permanently lost. The user has to manually search for the item again.
The Senior approach: You capture the intended destination before redirecting, route them through the Auth Gate, and automatically resume navigation upon success.
Here is what that looks like in code using GoRouter's centralized redirect callback:
final router = GoRouter(
initialLocation: '/',
// Re-evaluate redirect whenever auth state changes
refreshListenable: authStateNotifier,
redirect: (context, state) {
final isLoggedIn = authStateNotifier.isLoggedIn;
final isGoingToLogin = state.uri.path == '/login';
// Scenario 1: Not logged in, trying to access a protected route
if (!isLoggedIn && !isGoingToLogin) {
// Pass the intended destination as a query parameter
final targetUrl = Uri.encodeComponent(state.uri.toString());
return '/login?redirect=$targetUrl';
}
// Scenario 2: Logged in, currently on the login screen
if (isLoggedIn && isGoingToLogin) {
// Extract the saved target, or fallback to home
final redirectUrl = state.uri.queryParameters['redirect'];
if (redirectUrl != null) {
return Uri.decodeComponent(redirectUrl);
}
return '/home';
}
// Scenario 3: All good, proceed as normal
return null;
},
routes: [ ... ],
);
06. Declarative is Still Imperative Underneath
To wrap up this topic: notice how GoRouter's declarative `redirect` function is ultimately just expressing the exact same "check state before allowing navigation" imperative logic you would have written by hand in Navigator 1.0.
The difference isn't that declarative routing magically removes complexity. The difference is that declarative routing forces you to put that complexity in one centralized place, tied explicitly to your application's state, rather than scattering `if (isLoggedIn) Navigator.push(...)` checks across fifty different UI callbacks.
Expect these pushbacks when discussing routing:
- "What happens to a deep link if the user taps it while logged out?"
Answer: "Our GoRouter redirect intercepts it, sends them to `/login`, and appends their intended path as a `?redirect=` query parameter. After login succeeds, the router reads that query parameter and resumes navigation to their destination." - "Why not just use Navigator 1.0 if our app doesn't have web support?"
Answer: "Even on mobile-only, Navigator 2.0 (via GoRouter) gives us native OS deep-linking support, stateful nested routing for bottom navigation bars, and a centralized place to enforce auth guards. Rebuilding those from scratch imperatively is error-prone."
Senior-Level Sample Answer
"When a user taps a deep link to a specific product page while logged out, the OS opens the app and passes the URL to our Router. We use GoRouter, which triggers its centralized redirect callback. The router checks our auth state, sees the user is unauthenticated, and intercepts the navigation. Instead of dropping the link, it encodes the intended product URL into a query parameter and redirects them to the login screen. Once the user authenticates, the redirect callback runs again, sees they are now logged in and on the login screen, extracts that saved query parameter, and seamlessly routes them to the original product page. We never lose the user's intent."Before moving to the next post, answer this out loud: If your iOS app opens a website inside a Safari view instead of navigating to your app natively when a link is clicked, what specific file is likely misconfigured on your server?
(Answer: The `apple-app-site-association` file located in the `.well-known` directory.)
Official Resources
- GoRouter package documentation (pub.dev)
- Android App Links Documentation
- Apple Universal Links Documentation

Comments
Post a Comment