📋 Screens, Lists & Navigation · Lesson 25
Navigation Component & Safe Args
Why the Navigation Component?
Manual FragmentManager
- replace() + addToBackStack everywhere
- Back stack bugs
- Toolbar title by hand
- Bundles with string keys
Navigation Component
- All screens in one graph
- Back and Up handled
- Toolbar, drawer, bottom nav auto-linked
- Type-safe arguments (Safe Args)
Dependencies
TOML + Kotlin DSL
# gradle/libs.versions.toml
navigation = "2.8.5"
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
// app/build.gradle.kts
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)The navigation graph
Right-click res → New → Android Resource File → type Navigation → nav_graph. The Design view shows screens as boxes and actions as arrows. This is the real graph of Student Diary:
XML – res/navigation/nav_graph.xml
<?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/nav_graph"
app:startDestination="@id/notesFragment">
<!-- Open the editor from anywhere (FAB, list item) -->
<action
android:id="@+id/action_global_editor"
app:destination="@id/editorFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<fragment
android:id="@+id/notesFragment"
android:name="com.codingeasily.studentdiary.ui.notes.NotesFragment"
android:label="@string/app_name" />
<!-- The same list fragment, reused with different arguments -->
<fragment
android:id="@+id/favoritesFragment"
android:name="com.codingeasily.studentdiary.ui.notes.NoteListFragment"
android:label="@string/menu_favourites">
<argument
android:name="filter"
android:defaultValue="FAVORITES"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/subjectsFragment"
android:name="com.codingeasily.studentdiary.ui.subjects.SubjectsFragment"
android:label="@string/menu_subjects">
<action
android:id="@+id/action_subjects_to_subjectNotes"
app:destination="@id/subjectNotesFragment" />
</fragment>
<fragment
android:id="@+id/subjectNotesFragment"
android:name="com.codingeasily.studentdiary.ui.notes.NoteListFragment"
android:label="{subject}">
<argument
android:name="filter"
android:defaultValue="SUBJECT"
app:argType="string" />
<argument
android:name="subject"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/statsFragment"
android:name="com.codingeasily.studentdiary.ui.stats.StatsFragment"
android:label="@string/menu_stats" />
<fragment
android:id="@+id/settingsFragment"
android:name="com.codingeasily.studentdiary.ui.settings.SettingsFragment"
android:label="@string/menu_settings" />
<fragment
android:id="@+id/helpFragment"
android:name="com.codingeasily.studentdiary.ui.help.HelpFragment"
android:label="@string/menu_help" />
<fragment
android:id="@+id/editorFragment"
android:name="com.codingeasily.studentdiary.ui.editor.AddEditNoteFragment"
android:label="@string/note">
<argument
android:name="noteId"
android:defaultValue="-1L"
app:argType="long" />
</fragment>
</navigation>NavHostFragment
XML
<!-- activity_main.xml (Student Diary, part) -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />app:defaultNavHost="true" lets the system Back button go back inside the graph.
Navigate with actions
Kotlin
// Student Diary – NoteListFragment: open the editor for a note
findNavController().navigate(R.id.action_global_editor, bundleOf("noteId" to note.id))
// MainActivity – FAB opens an empty editor (noteId keeps its default -1L)
binding.fab.setOnClickListener { navController.navigate(R.id.action_global_editor) }
// SubjectsFragment – open notes of one subject
findNavController().navigate(
R.id.action_subjects_to_subjectNotes,
bundleOf("filter" to "SUBJECT", "subject" to item.subject)
)
// Go back
findNavController().navigateUp()Arguments (Bundle way)
Kotlin
// AddEditNoteFragment
val noteId = arguments?.getLong("noteId", -1L) ?: -1L
if (noteId != -1L) loadNote(noteId) else showEmptyForm()Default values from the graph (like -1L) are used when the caller passes nothing.
Safe Args (type-safe way)
Kotlin DSL – add the plugin
// build.gradle.kts (root)
plugins {
// ...
id("androidx.navigation.safeargs.kotlin") version "2.8.5" apply false
}
// app/build.gradle.kts
plugins {
// ...
id("androidx.navigation.safeargs.kotlin")
}Kotlin – generated Directions and Args
// nav_graph.xml has the global action action_global_editor → editorFragment // and editorFragment declares <argument android:name="noteId" app:argType="long" android:defaultValue="-1L"/> // Generated for global actions: NavGraphDirections (named after the graph id "nav_graph") val action = NavGraphDirections.actionGlobalEditor(noteId = note.id) findNavController().navigate(action) // Generated for the destination class: AddEditNoteFragmentArgs private val args: AddEditNoteFragmentArgs by navArgs() val noteId: Long = args.noteId // a Long – checked by the compiler
Toolbar Up button
Kotlin – MainActivity
// MainActivity (Student Diary)
val navHost = supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
navController = navHost.navController
appBarConfiguration = AppBarConfiguration(topLevelIds, binding.drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration) // title + ← / ☰
override fun onSupportNavigateUp(): Boolean =
navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
// react to screen changes (hide bottom bar on the editor)
navController.addOnDestinationChangedListener { _, destination, _ ->
binding.bottomNav.isVisible = destination.id in bottomScreens
}Common mistakes
- Menu item ids that do not match destination ids → bottom nav / drawer clicks do nothing.
- Calling
navigate()twice quickly (double tap) → "navigation destination is unknown" crash. Ignore extra clicks or check the current destination. - Forgetting
onSupportNavigateUp→ the ← button does nothing. - Wrong default format for long arguments → write
-1L.
Summary
- nav_graph.xml lists destinations, actions and arguments.
- NavHostFragment hosts the screens;
findNavController().navigate(id, bundle)moves between them. - Safe Args generates type-safe Directions/Args;
setupActionBarWithNavControllerhandles titles and Up.
✅ Practice
Convert your fragment demo to a nav graph: Home → Details (argument: subject) → Edit. Add Safe Args and an enter/exit animation to the actions.
Frequently Asked Questions
What is a destination?
A screen in the navigation graph, usually a fragment. Each destination has an id that you navigate to.
Is Safe Args required?
No. You can pass a Bundle with bundleOf(). Safe Args generates classes so the compiler checks argument names and types, which prevents typos.
How do I go back in code?
Call findNavController().navigateUp() or popBackStack(). Both return to the previous destination.
