Skip to content

Latest commit

 

History

History
85 lines (65 loc) · 6.2 KB

File metadata and controls

85 lines (65 loc) · 6.2 KB

Mobile Navigation Architecture Deep Dive

Navigation is one of the most critical and complex aspects of mobile system design. As apps grow, tight coupling between screens ("Feature A imports Feature B") creates a monolithic spaghetti code structure that is hard to test, refactor, and modularize.

The Challenge: How do you design a navigation system that decouples feature modules, supports deep linking, handles complex flows (e.g., authentication), and maintains state across configuration changes?

1. Core Requirements

  • Decoupling: Module A should navigate to Module B without knowing Module B exists.
  • Deep Linking: The app must be able to open any specific screen from a URL (e.g., app://profile/123).
  • Result Handling: Screen B needs to return data to Screen A (e.g., selecting a contact).
  • Back Stack Management: Handling the hardware back button (Android) or swipe-to-back (iOS) correctly.
  • Context/State Preservation: Restoring the navigation stack after a process death.

2. Evolution of Navigation Patterns

2.1 Direct Coupling (The "Junior" Approach)

  • Concept: Screen A directly imports and instantiates Screen B.
    • Example: Navigator.push(new DetailScreen(id))
  • The Problem: Tight Coupling. The "Feed" module must compile the "Detail" module. You cannot split them into separate build modules easily. Circular dependencies arise quickly (Profile -> Feed -> Profile), leading to a monolithic architecture that scales poorly.

2.2 The Coordinator Pattern

A dedicated object handles the flow logic, removing it from the UI components.

  • How it works:
    1. FeedScreen calls coordinator.showDetail(id).
    2. FeedCoordinator knows how to assemble the DetailScreen and its dependencies.
    3. FeedCoordinator pushes it onto the navigation stack.
  • Pros: Isolates navigation logic from UI logic. Excellent for unit testing navigation flows.
  • Cons: Coordinators can become "God Objects" if not split properly. Still requires the Coordinator to know about the destination classes unless combined with Dependency Injection.

2.3 The Router / URI-Based Navigation (The "Scalable" Approach)

Every screen is a URL.

  • How it works:
    1. FeedModule asks: Router.navigate("app://detail?id=123").
    2. Router looks up the registration map.
    3. Router instantiates the target screen and pushes it.
  • Pros: Zero compile-time coupling. Modules only know the Router interface. Identical logic for internal navigation and external Deep Links.
  • Cons: Loss of type safety (passing strings instead of objects).

3. Deep Dive: Modern Navigation Architecture

3.1 The Declarative Navigation Graph

  • Concept: A centralized file (JSON/XML/DSL) defines all possible destinations and paths ("Actions").
  • The Signal: This treats navigation as a State Machine. You define the states (Screens) and transitions (Actions).
  • Type Safety: Modern tools generate code from this graph to ensure type safety for arguments, fixing the main downside of loose URI-based navigation.
  • Modularization: Large graphs can be split into "Nested Graphs" (e.g., LoginGraph, CheckoutGraph), allowing different teams to own different parts of the navigation flow.

3.2 Feature-Based Navigation (Multi-Module)

In a modularized app where FeedModule and SettingsModule are separate binaries that don't know about each other, how do they navigate?

Option A: Interface Injection (The Clean Architecture Way)

  1. Define an interface in a shared core module: interface FeedNavigator { fun goToSettings() }.
  2. FeedModule depends on FeedNavigator.
  3. The main AppModule (which composes everything) implements FeedNavigator and injects it into FeedModule.

Option B: Implicit Deep Links (The Loose Coupling Way)

  1. FeedModule asks the Router to navigate to a generic URI: app://settings.
  2. The Router resolves this string to the SettingsScreen at runtime.
  3. Trade-off: Loss of compile-time safety (typos in strings cause runtime failures).

3.4 The Back Stack & Deep Linking

A common interview question: "If a user opens a Deep Link to app://profile/settings, what happens when they press Back?"

  • The Wrong Answer: "The app closes." (This feels broken to the user).
  • The Right Answer (Synthetic Back Stack): The navigation system must recognize that Settings belongs to a hierarchy. It should synthesize the parent screens (Home -> Profile -> Settings) onto the back stack so the user navigates "Up" the hierarchy, preserving the expected user flow.

4. Common Pitfalls ("Red Flags")

  • The "God" Router: Handling all navigation logic in a single file or class. Split logic into sub-routers or coordinators per feature.
  • Circular Dependencies: Feature A importing Feature B directly, creating a monolithic build graph that prevents parallel compilation.
  • Ignoring Process Death: Failing to save and restore the navigation stack state. If the OS kills the app to save memory, the user must return to the exact same screen hierarchy, not the home screen.
  • Stringly-Typed Navigation: Using raw strings ("app://detail") everywhere without a centralized constant file or builder pattern. This leads to runtime crashes from simple typos.

5. Summary Checklist for the Interview

  1. Decoupling Strategy: "I will use a Router pattern (or Coordinators) to ensure Feature A doesn't depend on Feature B."
  2. Deep Linking: "I will treat internal navigation and external deep links uniformly using a centralized URL resolver."
  3. Type Safety: "I will use code generation (like SafeArgs or a custom struct generator) to ensure parameter type safety between decoupled modules."
  4. State Restoration: "I will ensure the Router can serialize the back stack to handle process death."

6. Further Reading