Question
Android Navigation Safe Args Directions Classes in Kotlin
Question
I am using the Android Navigation component and Safe Args to pass data. My navigation graph defines an argument for a fragment:
<fragment
android:id="@+id/otherFragment"
android:name="com.asd.navigate.OtherFragment"
android:label="OtherFragment">
<argument
android:name="screenTitle"
android:defaultValue="0"
app:argType="string" />
</fragment>
OtherFragmentArgs is generated and usable, but no OtherFragmentDirections class is generated after building the project. Is this a Safe Args bug, or must I define something else?
My Gradle setup includes the Safe Args plugin:
buildscript {
dependencies {
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01"
}
}
apply plugin: "androidx.navigation.safeargs"
Short Answer
Safe Args generates different classes for two different jobs: Args classes read arguments received by a destination, while Directions classes create navigation actions that send arguments. You will learn why an argument alone creates an Args class, how to define an action that produces a Directions class, and how to navigate safely from Kotlin.
Concept
Safe Args is code generation for the Android Navigation component. It reads your navigation graph XML during the build and produces Kotlin or Java APIs that make navigation arguments type-safe.
Two generated APIs are especially important:
OtherFragmentArgsrepresents data that arrives atOtherFragment.SomeFragmentDirectionsrepresents navigation actions that leave fromSomeFragment.
An <argument> inside a <fragment> declares that the fragment can receive a value. Therefore, Safe Args can generate OtherFragmentArgs immediately.
A Directions class is different: it is based on one or more <action> elements. Your graph currently declares a destination and an argument, but it does not declare an action from any source destination to otherFragment. With no outgoing action, Safe Args has no direction method to generate.
Also note the naming: Safe Args normally generates OtherFragmentDirections (plural), not OtherFragmentDirection.
In the original XML, use app:argType="string" for the argument type. The android:defaultValue attribute is valid for the default value, but app:type is not the Safe Args argument-type attribute.
Mental Model
Think of fragments as rooms in a building.
- An argument is a form placed inside a room. When somebody enters, the room can read the form. That is
OtherFragmentArgs. - An action is a labeled route leaving a room, such as “Go to Other Fragment.” That route can carry a completed form. That is a method in
SomeFragmentDirections.
Putting a form in OtherFragment does not automatically tell the navigation system which room people will travel from. You must add a route—an <action>—to create a directions method.
Syntax and Examples
Declare the receiving destination and its argument:
<fragment
android:id="@+id/otherFragment"
android:name="com.asd.navigate.OtherFragment"
android:label="OtherFragment">
<argument
android:name="screenTitle"
android:defaultValue="Untitled"
app:argType="string" />
</fragment>
This produces an OtherFragmentArgs class. In OtherFragment, read the value with the Navigation Fragment Kotlin extension:
class OtherFragment : Fragment(R.layout.fragment_other) {
private val args: OtherFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val title = args.screenTitle
requireActivity().title = title
}
}
To generate a directions class, define a source fragment and add an action to the destination:
<fragment
android:id="@+id/homeFragment"
android:name="com.asd.navigate.HomeFragment"
android:label="Home">
<action
android:id="@+id/action_homeFragment_to_otherFragment"
app:destination="@id/otherFragment" />
</fragment>
<fragment
android:id="@+id/otherFragment"
android:name="com.asd.navigate.OtherFragment"
android:label="OtherFragment">
<argument
android:name="screenTitle"
android:defaultValue="Untitled"
app:argType="string" />
</fragment>
After rebuilding, Safe Args generates HomeFragmentDirections. Use it from HomeFragment:
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToOtherFragment(
screenTitle = "Account settings"
)
)
The directions class belongs to the source fragment because that fragment owns the outgoing action.
Step by Step Execution
Consider this graph:
<fragment
android:id="@+id/homeFragment"
android:name="com.asd.navigate.HomeFragment">
<action
android:id="@+id/action_homeFragment_to_otherFragment"
app:destination="@id/otherFragment" />
</fragment>
<fragment
android:id="@+id/otherFragment"
android:name="com.asd.navigate.OtherFragment">
<argument
android:name="screenTitle"
app:argType="string" />
</fragment>
And this Kotlin code:
val direction = HomeFragmentDirections
.actionHomeFragmentToOtherFragment("Welcome")
findNavController().navigate(direction)
Execution flow:
- The build reads the navigation graph.
- It sees
screenTitleonotherFragment, so it generatesOtherFragmentArgs. - It sees an action leaving
homeFragment, so it generatesHomeFragmentDirections. actionHomeFragmentToOtherFragment("Welcome")creates a direction object containing the destination ID andscreenTitlevalue.navigate(direction)asks theNavControllerto follow that action.- Android displays
OtherFragmentand stores the value as navigation arguments. OtherFragmentArgs.fromBundle(...), orby navArgs(), readsscreenTitleas aString.
If the action does not exist, step 3 cannot happen. That is why no directions class is generated.
Real World Use Cases
Safe Args directions are useful whenever one screen starts navigation and must provide typed input to the next screen.
- Product details:
ProductListFragmentDirections.actionProductListToDetails(productId) - Profile editing: pass a user ID from a profile screen to an edit screen.
- Search results: pass a query string and optional filter values.
- Checkout: pass a cart ID rather than a large mutable cart object.
- Notifications and deep links: use destination arguments to identify the content a user should see.
Arguments keep navigation contracts visible in the navigation graph and reduce errors caused by manually typed argument keys.
Real Codebase Usage
In real projects, developers generally define actions for normal screen-to-screen flows and use generated directions at the navigation call site.
A common click-handler pattern is:
binding.settingsButton.setOnClickListener {
val direction = HomeFragmentDirections
.actionHomeFragmentToOtherFragment("Settings")
findNavController().navigate(direction)
}
At the destination, read arguments once and validate important values early:
private val args: OtherFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val title = args.screenTitle
if (title.isBlank()) return
binding.title.text = title
}
Useful project conventions include:
- Pass stable identifiers such as
productIdoruserId, then load the full data through a repository or ViewModel. - Keep argument names descriptive and consistent, such as
orderIdrather thanid. - Define actions at the source destination when the flow is specific to that screen.
- Use destination-level arguments when several actions or deep links can open the same destination.
- Avoid navigating twice from rapid taps; apps often disable the triggering view briefly or check the current destination.
Common Mistakes
Expecting a directions class from an argument alone
This destination can generate OtherFragmentArgs, but it has no outgoing action:
<fragment
android:id="@+id/otherFragment"
android:name="com.asd.navigate.OtherFragment">
<argument
android:name="screenTitle"
app:argType="string" />
</fragment>
Fix: Add an <action> under the fragment that starts the navigation. The resulting directions class belongs to that source fragment.
Looking for the wrong name
This is incorrect:
OtherFragmentDirection
Safe Args uses the plural class name:
OtherFragmentDirections
However, it is generated only if OtherFragment itself has outgoing actions.
Expecting the destination to own the action
For this action:
<action
android:id="@+id/action_homeFragment_to_otherFragment"
app:destination="@id/otherFragment" />
the generated class is HomeFragmentDirections, not OtherFragmentDirections.
Using the wrong argument attribute
This is not the Safe Args type attribute:
<argument
android:name="screenTitle"
app:type="string" />
Use app:argType:
<argument
android:name="screenTitle"
app:argType="string" />
Mixing old and new Navigation dependencies
The question uses an old alpha-era plugin coordinate. In a current project, keep all Navigation libraries and the Safe Args plugin on compatible versions, and use the plugin ID recommended by the current Android Navigation documentation. Dependency or plugin-version mismatches can prevent generated code from appearing.
Importing the wrong generated package
Generated classes are placed in a package based on the navigation graph and application namespace. Let Android Studio add the import, or inspect the generated sources if the class exists but is not found by an import.
Comparisons
| Item | What generates it | Where it is used | Typical example |
|---|---|---|---|
OtherFragmentArgs | <argument> on otherFragment | Inside OtherFragment | args.screenTitle |
HomeFragmentDirections | <action> inside homeFragment | Inside HomeFragment | actionHomeFragmentToOtherFragment(...) |
NavController.navigate(@IdRes id) | A destination or action ID | Any navigation caller | navigate(R.id.otherFragment) |
NavController.navigate(direction) | A Safe Args direction object | Source destination | navigate(HomeFragmentDirections...) |
Using raw IDs can work, but Safe Args directions provide compile-time checking for required arguments and their types. A manually built Bundle is flexible, but it depends on string keys and can fail at runtime if keys or types do not match.
Cheat Sheet
<argument>on a destination generatesDestinationNameArgs.<action>inside a source destination generatesSourceNameDirections.- Directions class names are plural:
HomeFragmentDirections. - Define
app:destination="@id/targetFragment"on an action. - Define argument types with
app:argType, for exampleapp:argType="string". - Read arguments in a Fragment with:
private val args: OtherFragmentArgs by navArgs()
- Navigate with:
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToOtherFragment("Title")
)
- A destination with only incoming actions may have an
Argsclass but noDirectionsclass. - Rebuild after changing the navigation graph if generated sources are not immediately visible.
FAQ
Why does Safe Args generate Args but not Directions?
Args is generated from destination arguments. Directions is generated from actions declared on a source destination. Add an <action> to generate a directions class.
Should I look for OtherFragmentDirection or OtherFragmentDirections?
Look for OtherFragmentDirections. The generated class name is plural.
Why is HomeFragmentDirections generated instead of OtherFragmentDirections?
The action is declared inside homeFragment. Directions APIs belong to the destination where navigation begins, not where it ends.
Can I navigate to a fragment without a directions class?
Yes. You can call navigate() with an action or destination ID and a Bundle. Directions are the safer option when you want typed generated APIs.
Does every fragment generate a Directions class?
No. A fragment generates a directions class only when it contains at least one outgoing <action>.
How do I pass an argument with a Safe Args action?
Declare the argument on the target fragment, then call the generated action method with the required value:
HomeFragmentDirections.actionHomeFragmentToOtherFragment("Hello")
What does app:argType do?
It declares the type of a navigation argument, such as string, integer, boolean, or a supported custom type. Safe Args uses it to generate a typed method parameter and typed argument reader.
Mini Project
Description
Create a small two-screen app flow: a home screen opens a greeting screen and sends the name entered by the user. This demonstrates the complete Safe Args contract: an action at the source, an argument at the destination, generated directions for sending, and generated args for receiving.
Goal
Navigate from HomeFragment to GreetingFragment and display a greeting using a type-safe name argument.
Requirements
Add an action from HomeFragment to GreetingFragment. Declare a required string argument named name on GreetingFragment. Navigate when the user presses the greeting button. Pass the entered name through the generated directions method. Read the name with generated arguments and show it on the greeting screen.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.