Navigation Compose vs a single-Activity/multi-Fragment setup — what changed?
Fragments, the nav graph XML, and Safe Args solved 'one Activity, many screens' for a decade — Navigation Compose replaces the graph, the args, and the Fragment lifecycle itself with functions and typed routes.
LEARN BY DOINGPut your intuition to work3 steps
Navigation Compose vs a single-Activity/multi-Fragment setup — what changed?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The problem both approaches solve
Before either of these existed, many apps used one Activity per screen, which meant every screen transition was a full Activity transaction — its own window, its own Intent, its own onCreate, shared state passed through Intent extras (which meant Parcelable/Serializable everywhere) or a singleton. That's slow, memory-heavy, and awkward for shared UI chrome like a persistent bottom nav bar or toolbar.
The single-Activity, multi-Fragment architecture (Jetpack Navigation Component, roughly 2018 onward) fixed that: one Activity hosts a NavHostFragment, and screens become Fragments swapped in and out of that host. A nav_graph.xml resource declares every destination and the actions between them, and the Navigation Gradle plugin generates type-safe argument classes (Safe Args) so you don't pass a raw Bundle and hope the receiving Fragment reads the right keys.
<!-- nav_graph.xml --><fragmentandroid:id="@+id/homeFragment"android:name="com.example.app.HomeFragment"><actionandroid:id="@+id/action_home_to_profile"app:destination="@id/profileFragment" /></fragment><fragmentandroid:id="@+id/profileFragment"android:name="com.example.app.ProfileFragment"><argument android:name="userId" app:argType="string" /></fragment>
// Generated Safe Args in actionval action = HomeFragmentDirections.actionHomeToProfile(userId = clickedId)findNavController().navigate(action)
That was a real improvement — one back stack, one NavController, deep links declared in the same graph — but it kept every screen as a Fragment, with the full Fragment lifecycle (onCreate, onCreateView, onViewCreated, onDestroyView — note not onDestroy — separately from the Fragment instance's own lifecycle) and its well-known footguns: view references that must be nulled out in onDestroyView to avoid leaking the old view hierarchy, viewLifecycleOwner vs. the Fragment's own lifecycle for LiveData observers, and FragmentManager transactions that could throw IllegalStateException if committed after onSaveInstanceState.
#What Navigation Compose actually changes
Navigation Compose (part of Jetpack Compose, standard since roughly 2021, with typed routes since Navigation 2.8 in 2024) keeps the same core idea — one NavController, one back stack, a graph of destinations — but every destination is now a @Composable function, not a Fragment. That single change removes an entire category of bugs, because composables don't have a "view" separate from the composition to leak, and recomposition (not a two-phase view lifecycle) governs what's on screen.
@Serializableobject HomeRoute@Serializabledata class ProfileRoute(val userId: String)@Composablefun AppNavHost(navController: NavHostController = rememberNavController()) {NavHost(navController = navController, startDestination = HomeRoute) {composable<HomeRoute> {HomeScreen(onUserClick = { id -> navController.navigate(ProfileRoute(userId = id)) })}composable<ProfileRoute> { backStackEntry ->val route: ProfileRoute = backStackEntry.toRoute()ProfileScreen(userId = route.userId)}}}
Two things replace what Safe Args used to generate for you: routes are plain @Serializable Kotlin types (using kotlinx.serialization), so ProfileRoute(userId = id) is the type-safe argument — no codegen step, no separate XML resource to keep in sync with the Fragment class. And backStackEntry.toRoute<ProfileRoute>() deserializes it back out on the receiving side, fully typed, with a compile error if the shape ever drifts.
#What stayed conceptually the same
It's worth being precise about this in an interview, because the concepts barely changed even though the API did:
- Nested graphs still exist —
navigation<GraphRoute>(startDestination = ...) { ... }replaces the XML<navigation>tag, for grouping a flow (e.g. onboarding) behind its own sub-graph. - ViewModel scoping to a nav entry still works the same way —
hiltViewModel()inside a composable destination gives you a ViewModel scoped to that back stack entry, the direct analog of a Fragment-scoped ViewModel. - Deep links are still declared per destination (
deepLinks = listOf(navDeepLink { uriPattern = "..." })), and the back stack,popUpTo,saveState/restoreStatefor bottom-nav-style tab switching, and multiple back stacks all carry over conceptually unchanged. - Migration is incremental, not all-or-nothing:
ComposeViewlets you embed a composable inside an existing Fragment, andAndroidViewlets a Compose screen host a legacyView-based widget — most real migrations run both navigation systems side by side for a while, with oneActivityhosting either a Fragment-basedNavHostFragmentfor legacy screens or a ComposeNavHostfor new ones (rarely both graphs sharing one back stack, since that requires deliberate bridging).
🧠 Check your understanding
0/2 · 0/2 answered1. In Navigation Compose, what replaces the Safe-Args-generated argument classes?
2. Why does Navigation Compose avoid the 'view reference leaked after onDestroyView' bug class common in Fragments?