Navigation in XML Layouts: Passing Data Between Screens

In Android development using XML layouts, effectively navigating between screens and passing data is crucial for creating dynamic and interactive applications. While Jetpack Compose offers a modern approach to UI development, many existing projects still rely heavily on XML layouts. Understanding how to implement navigation and data passing in this traditional environment is essential for maintaining and updating these apps.

What is Screen Navigation?

Screen navigation refers to the process of moving between different activities or fragments in an Android application. It involves starting a new activity or replacing a fragment within an activity to display different user interfaces or data.

Why is Passing Data Between Screens Important?

Passing data between screens allows you to transfer information from one screen (Activity/Fragment) to another, maintaining context and enabling users to interact with relevant data as they navigate through your application. Without effective data passing, your app would feel disjointed and less user-friendly.

How to Implement Navigation and Pass Data in XML Layouts

Here’s a step-by-step guide on how to implement navigation and pass data between screens using XML layouts in Android.

Step 1: Set Up Activities

First, create two activities (ActivityA and ActivityB) along with their corresponding XML layout files (activity_a.xml and activity_b.xml).

ActivityA (MainActivity.kt):


package com.example.navigationxml

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val editTextData: EditText = findViewById(R.id.editTextData)
        val buttonNavigate: Button = findViewById(R.id.buttonNavigate)

        buttonNavigate.setOnClickListener {
            val data = editTextData.text.toString()
            val intent = Intent(this, SecondActivity::class.java)
            intent.putExtra("key_data", data) // Pass data using putExtra
            startActivity(intent)
        }
    }
}

ActivityB (SecondActivity.kt):


package com.example.navigationxml

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        val textViewReceivedData: TextView = findViewById(R.id.textViewReceivedData)

        // Get data from Intent extras
        val data = intent.getStringExtra("key_data")

        // Set the received data to the TextView
        textViewReceivedData.text = "Received Data: $data"
    }
}

In MainActivity, the Intent is used to navigate to SecondActivity. The putExtra method is used to pass the data entered in the EditText to the second activity. In SecondActivity, we retrieve the passed data using getStringExtra and display it in a TextView.

Step 2: Create XML Layout Files

Create the corresponding XML layout files for the activities.

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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editTextData"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:hint="Enter Data to Send"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <Button
        android:id="@+id/buttonNavigate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Navigate to Second Activity"
        android:layout_margin="16dp"
        app:layout_constraintTop_toBottomOf="@id/editTextData"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

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

    <TextView
        android:id="@+id/textViewReceivedData"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Received Data: "
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

These XML files define the UI layouts for MainActivity and SecondActivity, including an EditText for input in MainActivity and a TextView for displaying received data in SecondActivity.

Step 3: Add Intent Filters (Optional)

Intent filters are typically defined within the <activity> tag of your application’s AndroidManifest.xml file. These filters are crucial because they specify the types of implicit intents that an activity is capable of handling.

By adding an intent filter to an activity, you’re essentially advertising its ability to perform specific actions or handle particular types of data. For instance, an activity might declare that it can open web pages by specifying a filter for ACTION_VIEW with the appropriate data URI scheme (http or https). When another application or system component dispatches an implicit intent matching these criteria, the Android system presents the user with a choice of activities that can fulfill the request.

Activities can register multiple intent filters in their manifest to express different capabilities. In XML layouts and corresponding Kotlin classes:

AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.navigationxml">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.NavigationXML">
        <activity
            android:name=".SecondActivity"
            android:exported="false" />
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Step 4: Navigation with Fragments

Navigation between fragments involves using FragmentManager and FragmentTransaction. Fragments are reusable UI components that reside within an activity.

First, define your fragments (e.g., FirstFragment and SecondFragment) and their corresponding XML layouts (fragment_first.xml and fragment_second.xml).

FirstFragment.kt:


package com.example.navigationxml

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 android.widget.EditText
import androidx.core.os.bundleOf

class FirstFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_first, container, false)

        val editTextData: EditText = view.findViewById(R.id.editTextData)
        val buttonNavigate: Button = view.findViewById(R.id.buttonNavigate)

        buttonNavigate.setOnClickListener {
            val data = editTextData.text.toString()

            // Create a Bundle to pass data to the next fragment
            val bundle = bundleOf("key_data" to data)

            // Navigate to the SecondFragment
            val secondFragment = SecondFragment()
            secondFragment.arguments = bundle // Set the Bundle to the Fragment

            // Use FragmentTransaction to replace the current fragment with the SecondFragment
            val transaction = requireActivity().supportFragmentManager.beginTransaction()
            transaction.replace(R.id.fragmentContainer, secondFragment)
            transaction.addToBackStack(null) // Add the transaction to the back stack
            transaction.commit() // Commit the transaction
        }

        return view
    }
}

SecondFragment.kt:


package com.example.navigationxml

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

class SecondFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_second, container, false)

        val textViewReceivedData: TextView = view.findViewById(R.id.textViewReceivedData)

        // Get data from arguments Bundle
        val data = arguments?.getString("key_data")

        // Set the received data to the TextView
        textViewReceivedData.text = "Received Data: $data"

        return view
    }
}

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

    <EditText
        android:id="@+id/editTextData"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:hint="Enter Data to Send"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <Button
        android:id="@+id/buttonNavigate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Navigate to Second Fragment"
        android:layout_margin="16dp"
        app:layout_constraintTop_toBottomOf="@id/editTextData"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

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

    <TextView
        android:id="@+id/textViewReceivedData"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Received Data: "
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Finally, modify your activity’s layout file (activity_main.xml) to include a FrameLayout to host the fragments:


<?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">

    <FrameLayout
        android:id="@+id/fragmentContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

And load FirstFragment in your MainActivity:


package com.example.navigationxml

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Load FirstFragment
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragmentContainer, FirstFragment())
                .commit()
        }
    }
}

Step 5: Using Safe Args (Gradle Plugin)

Safe Args is a Gradle plugin that generates simple object and builder classes for type-safe navigation and argument passing between destinations.

Add Safe Args to your project

To add Safe Args to your project, include the following classpath in your top-level build.gradle or build.gradle.kts file:

build.gradle:


dependencies {
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
}

OR

build.gradle.kts:


dependencies {
    classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7")
}

To use Safe Args in your project, apply either the androidx.navigation.safeargs or androidx.navigation.safeargs.kotlin plugin to your build.gradle or build.gradle.kts file.

If using Kotlin,

build.gradle.kts:


plugins {
    id("androidx.navigation.safeargs.kotlin")
}

Otherwise if using Java:

build.gradle:


apply plugin: "androidx.navigation.safeargs"
Pass data with Navigation component

<fragment
    android:id="@+id/myFragment"
    android:name="com.example.MyFragment"
    tools:layout="@layout/my_fragment">
    <argument
        android:name="myArg"
        android:defaultValue="0"
        app:argType="integer" />
</fragment>

Best Practices for Screen Navigation and Data Passing

  • Use Intent Extras for Simple Data: For simple data types (strings, numbers, booleans), using Intent extras is efficient and straightforward.
  • Use Bundles for Complex Data: When passing more complex data or multiple values, use Bundles to group and organize the data.
  • Use ViewModel for UI-Related Data: For retaining UI-related data during configuration changes (like screen rotations), consider using ViewModels.
  • Use Serializable/Parcelable for Custom Objects: When passing custom objects between activities or fragments, implement the Serializable or Parcelable interface. Parcelable is generally more efficient for Android.

Conclusion

Effective navigation and data passing are vital for creating seamless and interactive Android applications using XML layouts. By following the step-by-step examples and best practices outlined in this guide, developers can ensure their apps provide a smooth and user-friendly experience, even as they transition to more modern UI development techniques.