Passing Data Between Fragments with SafeArgs in Android Navigation Component

Efficiently pass data between fragments in Android Navigation Component using SafeArgs. Follow our tutorial for a step-by-step example.

Passing data between fragments using android navigation SafeArgs example

Passing data between fragments is a common task in Android development. With the introduction of the Navigation Component in Android Jetpack, passing data between fragments has become easier and more efficient.

In this article, we will learn how to pass data between fragments using SafeArgs in Kotlin.

SafeArgs is a Gradle plugin that generates a class for each fragment with arguments that can be used to pass data between fragments. SafeArgs helps in type-safe argument passing and reduces the chances of errors while passing data between fragments.

Let’s get started by creating a new project in Android Studio and adding the Navigation Component to the project.

Create Project

Open Android Studio and create a new project. Give the project name SafeArgsDemo.

How to Create a New Project in Android Studio

After creating the project in Android Studio, add the Navigation Component to the project. To add the Navigation Component, add the following dependencies in the app-level build.gradle file.

Navigation Component dependency

dependencies {
    def nav_version = "2.3.5"

    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

SafeArgs plugin is included in the Navigation Component library and can be added to your project by adding the following dependency in your top-level build.gradle file:

Android safe-args dependency

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.5.3"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

And add the following plugin to your app-level build.gradle file.

plugins {
  id 'androidx.navigation.safeargs.kotlin'
}

Create two fragments, FragmentA and FragmentB, and add them to the navigation graph.

To create a navigation graph, create a new XML file named navigation.xml in the res folder of the project. Then, add the following code to the navigation.xml file.

navigation.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/navigation"
    app:startDestination="@id/fragment_a">

    <fragment
        android:id="@+id/fragment_a"
        android:name="com.example.safeargsdemo.FragmentA"
        tools:layout="@layout/fragment_a">

        <action
            android:id="@+id/action_fragment_a_to_fragment_b"
            app:destination="@id/fragment_b"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/fragment_b"
        android:name="com.example.safeargsdemo.FragmentB"
        tools:layout="@layout/fragment_b">

        <argument
            android:name="msg"
            app:argType="string" />
    </fragment>
</navigation>

In the above code, we have added two fragments, FragmentA and FragmentB, to the navigation graph. The start destination of the graph is FragmentA.

In the above code, we have added an argument named msg of type string to our FragmentB. This argument will be used to pass data between fragments using SafeArgs.

The argument can be accessed in your code using the FragmentBArgs class, which is automatically generated by the SafeArgs plugin.

In FragmentA, add a button and navigate to FragmentB on the button click.

FragmentA.kt

package com.example.safeargsdemo

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

class FragmentA : Fragment() {
    private var _binding: FragmentABinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        _binding = FragmentABinding.inflate(layoutInflater)
        return binding.root
    }

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

        // Passing data on button click
        binding.sendData.setOnClickListener {
            // Getting data from edittext
            val dataEditText = binding.dataPassed.text.toString()

            // Sending the data to FragmentB using safeargs
            val direction = FragmentADirections.actionFragmentAToFragmentB(dataEditText)
            findNavController().navigate(direction)
        }
    }

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

In the above code, we have added a button in FragmentA and created an action to navigate to FragmentB with data. Here, data is a string that we want to pass to FragmentB.

In FragmentB, retrieve the data passed from FragmentA using SafeArgs.

FragmentB.kt

package com.example.safeargsdemo

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.example.safeargsdemo.databinding.FragmentBBinding

class FragmentB : Fragment() {
    private var _binding: FragmentBBinding? = null
    private val binding get() = _binding!!
    private val args: FragmentBArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        _binding = FragmentBBinding.inflate(layoutInflater)
        return binding.root
    }

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

        // Getting data
        val data = args.msg

        // Setting the data in textview
        binding.dataPassed.text = data

        // Closing the fragment on back press
        binding.toolbar.setNavigationOnClickListener {
            findNavController().popBackStack()
        }
    }

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

In the above code, we have retrieved the data passed from FragmentA using SafeArgs. We have used the navArgs() delegate to retrieve the data. navArgs() is a delegate that provides type-safe access to the arguments passed to the fragment.

Layout file of activity and fragments

Below are the layout XML files for FragmentA, FragmentB, and MainActivity. These files define the user interface for each component of the application

FragmentA layout XML file

This file defines the layout for FragmentA. It contains an EditText view to allow the user to enter some text, and a Button view to submit the text to FragmentB.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FragmentA">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="0dp"
        android:fitsSystemWindows="true"
        app:liftOnScroll="false">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:title="FragmentA" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="24dp"
            android:orientation="vertical">

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Enter Message">

             <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/data_passed"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="textPersonName|textMultiLine" />
            </com.google.android.material.textfield.TextInputLayout>

            <Button
                android:id="@+id/send_data"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginVertical="24dp"
                android:text="Send to FragmentB" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

In the above code, we have created an EditText and a Button. When the user enters some text into the EditText and clicks the Button, FragmentB will open and display the data that was entered into the EditText.

FragmentB layout XML file

This file defines the layout for FragmentB. It contains a TextView view to display the text that was entered in FragmentA.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FragmentB">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="0dp"
        android:fitsSystemWindows="true"
        app:liftOnScroll="false">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:navigationIcon="@drawable/round_arrow_back_24"
            app:title="FragmentB" />
    </com.google.android.material.appbar.AppBarLayout>

    <TextView
        android:id="@+id/data_passed"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Passed data"
        android:textAppearance="?attr/textAppearanceTitleLarge"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

In the above code, we have created a TextView. When the user clicks the Button in Fragment A, Fragment B will open and display the data in the TextView.

MainActivity layout XML file

This file defines the layout for the MainActivity. It contains a FragmentContainerView that serves as the container for both FragmentA and FragmentB.

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_host_main"
        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/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

Testing Your App

Build the project and run it. Click on the button in FragmentA, and you will see the data “Hey, I am from FragmentA” displayed in FragmentB.

Android Navigation SafeArgs screenshot
Android Navigation SafeArgs Demo

That’s it! We have successfully passed data between fragments using SafeArgs in Kotlin.

Benefits of SafeArgs

SafeArgs provides several benefits, including:

Type-safety: SafeArgs generates a class for each fragment with arguments that can be used to pass data between fragments. This ensures that the data passed between fragments is of the correct type and reduces the chances of runtime errors.

Efficiency: SafeArgs generates code that is optimized for performance, making it more efficient than other methods of passing data between fragments.

Error-free: Since SafeArgs generates code at compile-time, it reduces the chances of errors that may occur while passing data between fragments.

In conclusion, SafeArgs provides a type-safe way to pass data between fragments in the Navigation Component.

It eliminates the need for writing boilerplate code and reduces the chances of errors while passing data between fragments.

With SafeArgs, passing data between fragments has become easier and more efficient.

I hope this article helps you understand how to pass data between fragments in the navigation component using Android SafeArgs.

So, go ahead and try it out today!