Droid Manual // Android Knowledge Base

Droid Manual

Android Navigation Component Guide

October 29, 2026 · 10 min read

Introduction

The Navigation Component is one of the most powerful libraries in the Android Jetpack family. It solves a problem that has haunted Android developers for years: managing back stacks, fragment transactions, and argument passing across screens in a consistent, type-safe way. Instead of manually calling FragmentManager and writing boilerplate transaction code, you declare your app's navigation structure in a single XML graph and let the library handle the rest. This guide walks through the core features you need to build real-world apps—navigation graphs, Safe Args, deep links, nested graphs, and bottom navigation—with Kotlin and XML.


Setting Up the Navigation Component

Before you can use the Navigation Component, you need to add the right dependencies to your project. The library consists of several artifacts: the core navigation runtime, fragment support, and the Safe Args Gradle plugin that generates type-safe argument classes.

// build.gradle (project level)
plugins {
    id 'com.android.application' version '8.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '2.0.0' apply false
    id 'androidx.navigation.safeargs.kotlin' version '2.7.7' apply false
}
// build.gradle (app level)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'androidx.navigation.safeargs.kotlin'
}

dependencies {
    implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
    implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
    implementation 'androidx.navigation:navigation-dynamic-features-fragment:2.7.7'
    implementation 'androidx.activity:activity-ktx:1.8.2'
    implementation 'androidx.fragment:fragment-ktx:1.7.0'
}

The Safe Args plugin must be applied at both levels. The project-level apply false declaration makes the plugin available as a classpath dependency, while the app-level activation actually generates the argument holder classes during compilation. The fragment-ktx and ui-ktx artifacts are required because they provide Kotlin coroutine and extension-based APIs that you will use throughout the navigation flow.


Understanding NavGraph and Fragment Navigation

The navigation graph is an XML resource that defines every destination (fragment or activity), the actions that connect them, and the default start destination. You create it under res/navigation/, and the Navigation Component parses it at runtime to build a NavController that manages your back stack.

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.myapp.ui.home.HomeFragment"
        android:label="Home">
        <action
            android:id="@+id/action_homeToDetail"
            app:destination="@id/detailFragment"
            app:popEnterTransition="@android:anim/fade_in"
            app:popExitTransition="@android:anim/fade_out" />
        <action
            android:id="@+id/action_homeToSettings"
            app:destination="@id/settingsFragment" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.myapp.ui.detail.DetailFragment"
        android:label="Detail">
        <argument
            android:name="itemId"
            app:argType="string"
            app:nullable="false" />
        <argument
            android:name="isFavorite"
            app:argType="boolean"
            android:defaultValue="false" />
        <action
            android:id="@+id/action_detailToHome"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.myapp.ui.settings.SettingsFragment"
        android:label="Settings" />

</navigation>

This graph declares three fragments and two actions. The homeFragment acts as the start destination, meaning the NavController will navigate to it first when the app launches. The action action_homeToDetail moves the user to the detail screen and also defines Enter and Exit transitions that control how fragments animate into and out of view. The detailFragment exposes two arguments: itemId (required, non-nullable) and isFavorite (optional with a default value). The popUpTo and popUpToInclusive attributes on the detail-to-home action ensure that when the user presses back from the detail screen, the home fragment remains on the stack instead of revisiting the detail screen.


Safe Args for Type-Safe Arguments

One of the biggest sources of runtime crashes in Android apps is incorrect argument handling between fragments. Developers manually extract arguments using string keys, which means a typo or type mismatch causes a crash at navigation time rather than at compile time. Safe Args eliminates this risk by generating strongly typed accessor and builder classes for every destination in your graph.

// HomeFragment.kt
package com.example.myapp.ui.home

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.example.myapp.databinding.FragmentHomeBinding

class HomeFragment : Fragment() {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.btnOpenDetail.setOnClickListener {
            val itemId = "product_42"
            val isFavorite = true
            val navController = findNavController()
            val action = HomeFragmentDirections.actionHomeToDetail(itemId, isFavorite)
            navController.navigate(action)
        }

        binding.btnOpenSettings.setOnClickListener {
            val navController = findNavController()
            navController.navigate(HomeFragmentDirections.actionHomeToSettings())
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
// DetailFragment.kt
package com.example.myapp.ui.detail

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.example.myapp.databinding.FragmentDetailBinding

class DetailFragment : Fragment() {

    private var _binding: FragmentDetailBinding? = null
    private val binding get() = _binding!!

    private val args: DetailFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentDetailBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.tvItemId.text = args.itemId
        binding.tvFavoriteLabel.text = if (args.isFavorite) "Saved" else "Not Saved"

        binding.btnBack.setOnClickListener {
            findNavController().navigate(DetailFragmentDirections.actionDetailToHome())
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Safe Args generates a HomeFragmentDirections class with factory methods for every action defined in the graph. When you call actionHomeToDetail("product_42", true), the compiler verifies that the argument types match the graph definition. On the receiving end, navArgs() uses a delegate to fetch the arguments from the fragment's arguments bundle and exposes them as properties with the correct types. If you accidentally pass an integer where a string is expected, or forget a required argument, the code will not compile. This shifts errors from runtime to compile time, where they are infinitely cheaper to fix.


Deep Links

Deep linking lets external apps, browser links, or shortcuts open a specific screen inside your app with the right context. The Navigation Component supports deep links declaratively in the graph XML, so you do not need to intercept intents manually in each fragment. A deep link route is matched against an app:uri attribute on a destination, and the framework automatically navigates to that destination when the intent arrives.

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.myapp.ui.home.HomeFragment"
        android:label="Home" />

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.myapp.ui.detail.DetailFragment"
        android:label="Detail">
        <argument
            android:name="itemId"
            app:argType="string"
            app:nullable="false" />

        <deepLink app:uri="myapp://product/{itemId}" />

        <action
            android:id="@+id/action_detailToHome"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="true" />
    </fragment>

</navigation>
<!-- AndroidManifest.xml -->
<activity
    android:name="com.example.myapp.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="product" />
    </intent-filter>
</activity>

The manifest declares an intent filter that matches any URI with the scheme myapp and host product. When the system resolves a link like myapp://product/abc123, the Navigation Component parses the path segment into the itemId argument and navigates directly to detailFragment with that value already set. The {itemId} placeholder in the app:uri value maps to the argument name in the graph, so no manual intent parsing is required. The MainActivity must be the one hosting the NavHostFragment, because the NavController that handles deep links lives inside that fragment container.


Nested Navigation Graphs

As an app grows, a single navigation graph becomes unwieldy. Nested graphs let you split your navigation structure into logical modules—onboarding, main app, settings—while keeping each file focused and maintainable. A nested graph is imported into a parent graph using the <navigation> element with the app:graph attribute, and it behaves exactly like any other destination: you can navigate to it via an action, and it can have its own start destination and local back stack.

<!-- res/navigation/nav_main_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_main_graph"
    app:startDestination="@id/bottomNavigation">

    <item android:id="@+id/bottomNavigation"
        android:label="main"
        app:destination="@id/bottom_nav_host" />

    <navigation
        android:id="@+id/bottom_nav_host"
        app:startDestination="@id/homeFragment">

        <fragment
            android:id="@+id/homeFragment"
            android:name="com.example.myapp.ui.home.HomeFragment"
            android:label="Home" />

        <fragment
            android:id="@+id/profileFragment"
            android:name="com.example.myapp.ui.profile.ProfileFragment"
            android:label="Profile" />

    </navigation>

    <navigation
        android:id="@+id/settings_nav"
        app:startDestination="@id/settingsFragment"
        app:graph="@navigation/nav_settings_graph" />

</navigation>
<!-- res/navigation/nav_settings_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/settings_nav"
    app:startDestination="@id/settingsFragment">

    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.myapp.ui.settings.SettingsFragment"
        android:label="Settings">
        <action
            android:id="@+id/action_settingsToAccount"
            app:destination="@id/accountFragment" />
    </fragment>

    <fragment
        android:id="@+id/accountFragment"
        android:name="com.example.myapp.ui.settings.AccountFragment"
        android:label="Account" />

</navigation>
// MainActivity.kt
package com.example.myapp

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.example.myapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
        val navController = navHostFragment.navController
        binding.bottomNavigation.setupWithNavController(navController)
    }
}
<!-- res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/navHostFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_main_graph"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/bottomNavigation"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:menu="@menu/bottom_nav_menu"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<!-- res/menu/bottom_nav_menu.xml -->
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/homeFragment"
        android:icon="@android:drawable/ic_dialog_info"
        android:title="Home" />
    <item
        android:id="@+id/profileFragment"
        android:icon="@android:drawable/ic_menu_myplaces"
        android:title="Profile" />
</menu>

The bottom_nav_host item inside the main graph is itself a <navigation> element, which tells the parser to treat its children as a self-contained sub-graph. The settings_nav item references an external graph file via app:graph, demonstrating modular graph composition. In MainActivity, setupWithNavController binds the bottom navigation bar to the navigation controller so that selecting a tab navigates to the corresponding destination and the selected state updates automatically. Setting app:defaultNavHost="true" ensures the hardware back button routes through the NavController rather than finishing the activity directly.


Navigating with the NavController Programmatically

While the Navigation Component excels at XML-driven navigation, you will frequently need to trigger navigation from code—after a successful API call, a validation check, or a dialog confirmation. The NavController exposes several methods for this, each with different semantics around the back stack.

// HomeFragment.kt — navigation logic
package com.example.myapp.ui.home

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.example.myapp.databinding.FragmentHomeBinding

class HomeFragment : Fragment() {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.btnOpenDetail.setOnClickListener {
            val navController = findNavController()
            val bundle = bundleOf("itemId" to "product_42", "isFavorite" to true)
            navController.navigate(R.id.detailFragment, bundle)
        }

        binding.btnOpenSettings.setOnClickListener {
            val navController = findNavController()
            navController.navigate(HomeFragmentDirections.actionHomeToSettings())
        }

        binding.btnNavigateToProfile.setOnClickListener {
            val navController = findNavController()
            navController.navigate(HomeFragmentDirections.actionHomeToSettings())
            navController.popBackStack(R.id.settingsFragment, inclusive = false, saveState = true)
        }
    }

    override fun onResume() {
        super.onResume()
        val navController = findNavController()
        navController.addOnDestinationChangedListener { _, destination, _ ->
            binding.tvCurrentDestination.text = destination.label.toString()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

The navigate(@IdRes resId, @Nullable Bundle args) overload lets you jump to any destination by its ID without defining an explicit action in the graph, which is useful for direct jumps that do not need to be part of the flow diagram. The Directions object approach remains the recommended pattern for standard transitions because it embeds the navigation contract in the graph and is validated at compile time. The popBackStack call demonstrates how to manipulate the back stack programmatically—you can remove intermediate destinations, save state, and control whether the popped destination itself is removed with the inclusive flag. The addOnDestinationChangedListener callback gives you visibility into every navigation event, which is handy for updating UI elements like toolbar titles or tracking screens for analytics.


Common Pitfalls

Several recurring mistakes trip up developers who are new to the Navigation Component. The most common is forgetting to set app:defaultNavHost="true" on the NavHostFragment in your activity layout. Without this attribute, the hardware back button bypasses the NavController and calls Activity.finish() directly, which breaks the expected back navigation behavior and can leave the user stranded on a screen with no way to return.

Another frequent issue is the "duplicate entry" crash that occurs when the same fragment instance is pushed onto the back stack multiple times without proper popUpTo configuration. If you navigate from fragment A to fragment B and then from B back to A without clearing the intermediate state, pressing back will recreate A and show stale data. Using app:popUpTo with app:popUpToInclusive="true" on actions that should reset the stack prevents this.

Fragment instantiation errors often stem from missing no-argument constructors. Kotlin data classes and fragments with required constructor parameters that lack a zero-arg constructor will throw an exception when the Navigation Component attempts to instantiate them. Always ensure your fragments have a public no-argument constructor, either explicitly declared or implicitly provided by the compiler.

Finally, Safe Args requires the plugin to be applied before you can use the generated classes. If you see a "Unresolved reference" error for classes like HomeFragmentDirections or DetailFragmentArgs, verify that the androidx.navigation.safeargs.kotlin plugin is declared in your app-level build.gradle and that you have performed a clean rebuild after adding it.


Conclusion

The Navigation Component brings structure and safety to Android app navigation. By declaring your destinations, actions, and deep links in XML, you gain a visual map of your app's flow that the framework enforces at runtime. Safe Args shifts argument bugs from production crashes to compile-time errors, nested graphs keep large projects organized, and deep links integrate naturally without manual intent parsing. The patterns covered here—starting with dependency setup, moving through graph design, type-safe argument passing, deep linking, modular graphs, and bottom navigation—form the foundation for building robust, maintainable Android apps. As your app grows, the same principles scale: keep your graphs organized, prefer directed actions over raw IDs, and let the back stack management work for you rather than against you.