Introduction
When using Navigation Components, you might have wondered how to pass arguments between destinations. We will learn how to do just that in this tutorial. This tutorial also builds upon the Basic Navigation tutorial, so if you are not familiar with Navigation Components, I would recommend you to check it out.
There are two parts to this tutorial. First, we will learn how to perform navigation with type safety. Secondly, we will learn how to pass arguments with Safe Args.
Goals
At the end of the tutorial, you would have learned:
- How to use Navigation with Type Safety.
- How to pass arguments between destinations with Safe Args.
Tools Required
- Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 1.
Prerequisite Knowledge
- Basic Android.
- Basic Navigation.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Remove the default “Hello World!” TextView.
-
In the Project-level build.gradle file, add the
buildscript{}
block below. Make sure that you place it above the existingplugins{}
block.buildscript { repositories { google() } dependencies { def nav_version = "2.4.0" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }
-
In the Module-level build.gradle file, add the dependencies below to the
dependencies{}
block.def nav_version = "2.4.0" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
-
Also in the Module-level build.gradle file, add the plugin below into the
plugins{}
block.id 'androidx.navigation.safeargs.kotlin'
-
Because of this bug, you will have to downgrade your Android Gradle Plugin (AGP) to version 7.0.4. Gradle version 7.4 seems to work fine as of this tutorial. You can change your Project settings by going to File > Project Structure > Project.
-
Performs a Gradle sync.
-
In the same package as MainActivity.kt, create two new fragments called BlankFragment1.kt, BlankFragment2.kt along with their respective layout files fragment_blank1.xml and fragment_blank2.xml.
-
Create a new navigation graph called nav_graph.xml.
-
Replace the content of activity_main.xml with the code below.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragmentContainerView" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/nav_graph" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
Replace the content of nav_graph.xml with the code below.
<?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" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" app:startDestination="@id/blankFragment1"> <fragment android:id="@+id/blankFragment1" android:name="com.codelab.daniwebandroidtypesafenav.BlankFragment1" android:label="fragment_blank1" tools:layout="@layout/fragment_blank1" > <action android:id="@+id/action_blankFragment1_to_blankFragment2" app:destination="@id/blankFragment2" /> </fragment> <fragment android:id="@+id/blankFragment2" android:name="com.codelab.daniwebandroidtypesafenav.BlankFragment2" android:label="fragment_blank2" tools:layout="@layout/fragment_blank2" /> </navigation>
-
Add the string resource below into strings.xml.
<string name="second_screen">2nd Screen</string>
-
Replace the content of fragment_blank1.xml with the code below.
<?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" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BlankFragment1" > <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/second_screen" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Typesafe Navigation
Before diving into navigation type safety, we will first look at what navigation without type safety is like. Copy and paste the overridden version of onViewCreated()
below into BlankFragment1.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController()
val button = view.findViewById<Button>(R.id.button)
button.setOnClickListener {
//Will compile, but will crash at runtime. No Compiler checks (except for @IdRes).
navController.navigate(R.id.button)
//Will compile. Won't crash at runtime. No Compiler checks (except for @IdRes).
//navController.navigate(R.id.action_blankFragment1_to_blankFragment2)
//Will compile. Won't crash at runtime. Has Compiler checks
//val direction = BlankFragment1Directions.actionBlankFragment1ToBlankFragment2()
//navController.navigate(direction)
}
}
The code snippet above presents three different ways to navigate to a destination using a NavController. Without type safety, we normally can navigate using the android:id
of the navigation <action>
. When dissecting the method signature of the NavController#navigate()
,
public open fun navigate(@IdRes resId: Int)
then we can see that it will take just about any Int argument. We can pass a useful Int value from R.id
, or any number, really. There is an annotation @IdRes
applied to the parameter which helps a bit due to the IDE’s annotation processor. If you were to provide a non-id number to navigate()
, you will see some help from the IDE in the form of a compiler error, which can be suppressed.
Right now, we are passing to navigate()
a valid R.id.button
,
navController.navigate(R.id.button)
but it is not a valid navigation <action>
, so we will not see any compiler error. The code will compile, but it will crash at runtime when we press the Button.
java.lang.IllegalArgumentException: Navigation action/destination com.codelab.daniwebandroidtypesafenav:id/button cannot be found from the current destination
The second way to navigate using the <action>
android:id
is correct and will not crash, but it is still not typesafe.
navController.navigate(R.id.action_blankFragment1_to_blankFragment2)
Because we have added the safe-args dependencies into our project, we can now use navigation with type safety. Type safety is provided by the interface NavDirections. The safe-args dependencies will generate a class implementing NavDirections at development time for each originating Destination of an <action>
. This class will have the same name as the originating fragment, but with the word Directions
suffixed to it. This new class will also have methods with the same name as the <action>
android:id
, converted to camel case after stripping out underscores (_
).
You can now comment out the first two ways to navigate and uncomment the last way to navigate with NavDirections.
Pass data with Safe Args
In this section, we will learn how to pass arguments with Safe Args.
Replace the content of fragment_blank2.xml with the code below.
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment2">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Test Test Test" />
</androidx.constraintlayout.widget.ConstraintLayout>
To pass an argument to blankFragment2
when navigating to it from blankFragment1
, we will have to do a couple of things.
-
We have to add an
<argument>
element to the destination fragment (blankFragment2
). This will trigger the compiler to add a parameter to the methodactionBlankFragment1ToBlankFragment2()
, so it will becomeactionBlankFragment1ToBlankFragment2(sampleArgs: String = “default value”)
(if<argument>
has the attributeandroid:defaultValue
). In nav_graph.xml, inside ofblankFragment2
, add the<argument>
below.<argument android:name="sampleArg" app:argType="string" android:defaultValue="blank" />
-
You might have to rebuild the project for the compiler to generate the new class.
-
Back in the Button’s
onClickListener
callback, we can replace the previousdirection
with the code below to pass an argument.val direction = BlankFragment1Directions.actionBlankFragment1ToBlankFragment2("Arg from fragment1 to fragment2")
-
In order for BlankFragment2 to receive the arguments, we can use the
navArgs()
delegate fromandroidx.navigation.fragment
to receive a type of BlankFragment2Args, which the compiler has generated for us as well. In BlankFragment2.kt, copy and paste the code below.val args: BlankFragment2Args by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val textView = view.findViewById<TextView>(R.id.textView) textView.text = args.sampleArg }
And that is the last step in this tutorial.
Run the App
We are not ready to run the App. Your app should behave similarly to the Gif below.
Solution Code
BlankFragment1.kt
package com.codelab.daniwebandroidtypesafenav
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.navigation.fragment.findNavController
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment1.newInstance] factory method to
* create an instance of this fragment.
*/
class BlankFragment1 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController()
val button = view.findViewById<Button>(R.id.button)
button.setOnClickListener {
//Will compile, but will crash at runtime. No Compiler checks (except for @IdRes).
//navController.navigate(R.id.button)
//Will compile. Won't crash at runtime. No Compiler checks (except for @IdRes).
//navController.navigate(R.id.action_blankFragment1_to_blankFragment2)
//Will compile. Won't crash at runtime. Has Compiler checks
//val direction = BlankFragment1Directions.actionBlankFragment1ToBlankFragment2()
val direction = BlankFragment1Directions.actionBlankFragment1ToBlankFragment2("Arg from fragment1 to fragment2")
navController.navigate(direction)
}
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment1.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment1().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
BlankFragment2.kt
package com.codelab.daniwebandroidtypesafenav
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.navigation.fragment.navArgs
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment2.newInstance] factory method to
* create an instance of this fragment.
*/
class BlankFragment2 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank2, container, false)
}
val args: BlankFragment2Args by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = args.sampleArg
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment2.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment2().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_blank1.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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment1" >
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/second_screen"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_blank2.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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment2">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Test Test Test" />
</androidx.constraintlayout.widget.ConstraintLayout>
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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/blankFragment1">
<fragment
android:id="@+id/blankFragment1"
android:name="com.codelab.daniwebandroidtypesafenav.BlankFragment1"
android:label="fragment_blank1"
tools:layout="@layout/fragment_blank1" >
<action
android:id="@+id/action_blankFragment1_to_blankFragment2"
app:destination="@id/blankFragment2" />
</fragment>
<fragment
android:id="@+id/blankFragment2"
android:name="com.codelab.daniwebandroidtypesafenav.BlankFragment2"
android:label="fragment_blank2"
tools:layout="@layout/fragment_blank2" >
<argument
android:name="sampleArg"
app:argType="string"
android:defaultValue="blank" />
</fragment>
</navigation>
strings.xml
<resources>
<string name="app_name">Daniweb Android Typesafe Nav</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="second_screen">2nd Screen</string>
</resources>
Project build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
}
dependencies {
def nav_version = "2.4.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
plugins {
id 'com.android.application' version '7.0.4' apply false
id 'com.android.library' version '7.0.4' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'androidx.navigation.safeargs.kotlin'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.codelab.daniwebandroidtypesafenav"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
def nav_version = "2.4.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary
We have learned how to navigate with type safety and pass destination arguments in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidTypesafeNav.