Blogs/Technology

How to Pass Data Between Activities in Android Without Intent Boilerplate?

Written byMurtuza Kutub
Aug 10, 2026
7 Min Read
How to Pass Data Between Activities in Android Without Intent Boilerplate? Hero
Too Long? Read This First
- Use Intent.putExtra() to pass small amounts of data between activities.
- Use @Parcelize when a custom Kotlin object must be included.
- Keep extra keys inside a dedicated contract instead of repeating string literals.
- Provide one function for creating the Intent and another for reading its arguments.
- Treat required extras differently from optional extras.
- Pass an object ID instead of the complete object when the destination can reload it.
- Avoid Dart and Henson in new projects because they are no longer part of the modern Android toolchain.

Passing data between activities usually starts with a few Intent.putExtra() calls. As an application grows, however, extra keys become scattered across multiple files, required values become difficult to distinguish from optional ones, and type mismatches can cause runtime failures.

A simple Intent contract solves much of this problem without relying on an additional annotation-processing library. It keeps the keys, Intent creation, validation, and data extraction in one place.

How Intent Extras Work in Android

An explicit Intent identifies the activity Android should open. It can also contain a Bundle of additional values known as extras.

Here is the basic approach:

val intent = Intent(this, DetailsActivity::class.java).apply {
    putExtra("user_id", 42)
    putExtra("from_screen", "login")
    putExtra("is_success", true)
}

startActivity(intent)

The destination retrieves each value using the same key:

val userId = intent.getIntExtra("user_id", -1)
val fromScreen = intent.getStringExtra("from_screen")
val isSuccess = intent.getBooleanExtra("is_success", false)

This works, but the contract exists only through matching string literals. Renaming "user_id" on one side and forgetting the other does not produce a compilation error. It simply causes the destination to receive a missing or default value.

Reduce Intent Boilerplate with a Contract

A contract centralizes the rules for opening an activity. The calling activity no longer needs to know the extra keys or how the values are packaged.

Consider a details screen that requires a User, accepts an optional source screen, and uses a Boolean flag with a default value.

Step 1: Enable Kotlin Parcelize

Add the Parcelize plugin to the app module:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-parcelize")
}

The kotlin-parcelize plugin generates the Parcelable implementation during compilation. It avoids the manual writeToParcel() and CREATOR code previously required for custom objects.

Step 2: Create a Parcelable Data Class

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class User(
    val id: Long,
    val name: String,
    val email: String,
    val mobile: String
) : Parcelable

Parcelable is suitable for small objects passed through Android components. It should not be treated as a general-purpose storage or network serialization format.

Step 3: Define the Activity Arguments

Represent the destination’s input as a single object:

data class DetailsArgs(
    val user: User,
    val fromScreen: String?,
    val isSuccess: Boolean
)

This makes the expected values clear and gives the destination one structured result instead of several unrelated variables.

Step 4: Create the Intent Contract

import android.content.Context
import android.content.Intent
import androidx.core.content.IntentCompat

object DetailsActivityContract {

    private const val EXTRA_USER = "details.extra.USER"
    private const val EXTRA_FROM_SCREEN = "details.extra.FROM_SCREEN"
    private const val EXTRA_IS_SUCCESS = "details.extra.IS_SUCCESS"

    fun createIntent(
        context: Context,
        user: User,
        fromScreen: String? = null,
        isSuccess: Boolean = false
    ): Intent {
        return Intent(context, DetailsActivity::class.java).apply {
            putExtra(EXTRA_USER, user)
            putExtra(EXTRA_FROM_SCREEN, fromScreen)
            putExtra(EXTRA_IS_SUCCESS, isSuccess)
        }
    }

    fun readArgs(intent: Intent): DetailsArgs {
        val user = IntentCompat.getParcelableExtra(
            intent,
            EXTRA_USER,
            User::class.java
        ) ?: throw IllegalArgumentException(
            "DetailsActivity requires a User extra"
        )

        return DetailsArgs(
            user = user,
            fromScreen = intent.getStringExtra(EXTRA_FROM_SCREEN),
            isSuccess = intent.getBooleanExtra(EXTRA_IS_SUCCESS, false)
        )
    }
}

IntentCompat.getParcelableExtra() Handles Parcelable retrieval consistently across Android versions, including the typed API introduced in Android 13.

The namespaced keys also reduce the chance of collisions when an Intent contains extras from more than one source.

Send Data from the Source Activity

The source activity now uses one method:

val user = User(
    id = 42L,
    name = "Anita",
    email = "anita@example.com",
    mobile = "9876543210"
)

val intent = DetailsActivityContract.createIntent(
    context = this,
    user = user,
    fromScreen = "login",
    isSuccess = true
)

startActivity(intent)

There are no key strings or individual putExtra() calls outside the contract. If the destination’s requirements change, the contract provides a single place to update the calling API.

Optional parameters can be omitted:

startActivity(
    DetailsActivityContract.createIntent(
        context = this,
        user = user
    )
)

In this case, fromScreen is null, while isSuccess uses its default value of false.

Automate Your Android Workflow

We build Android Studio plugins that cut manual effort, improve speed, and make your development process seamless.

Receive Data in the Target Activity

Read and validate the extras when the activity is created:

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

class DetailsActivity : AppCompatActivity() {

    private lateinit var args: DetailsArgs

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_details)

        args = DetailsActivityContract.readArgs(intent)

        displayUser(args.user)

        if (args.isSuccess) {
            showSuccessMessage()
        }
    }

    private fun displayUser(user: User) {
        // Update the UI with the received user.
    }

    private fun showSuccessMessage() {
        // Display the appropriate success state.
    }
}

A missing required User now produces a clear error close to the activity boundary. This is easier to diagnose than allowing the value to remain null and fail later during UI rendering.

For an exported activity that other applications can launch, do not assume extras are trustworthy. Validate ranges, formats, identifiers, and any value used to perform a sensitive action.

Required, Optional, and Default Extras

The contract should make the behaviour of every argument obvious.

Required extras

A required extra should be validated immediately:

val user = IntentCompat.getParcelableExtra(
    intent,
    EXTRA_USER,
    User::class.java
) ?: throw IllegalArgumentException("User is required")

Failing early produces a useful message and prevents invalid activity state from spreading through the rest of the screen.

Optional extras

Nullable values can be returned normally:

val fromScreen = intent.getStringExtra(EXTRA_FROM_SCREEN)

The activity must then handle the null case intentionally.

Extras with default values

Primitive retrieval methods can provide defaults:

val isSuccess = intent.getBooleanExtra(EXTRA_IS_SUCCESS, false)

Choose a default that produces safe and predictable behaviour. Avoid values such as 0 or false if they could accidentally represent a valid, sensitive operation.

Should You Pass the Complete Object or Only Its ID?

Although Parcelable makes passing objects convenient, it is not always the best architectural choice.

Pass the object when:

  • The object is small.
  • The destination needs a temporary snapshot.
  • Reloading the data would add unnecessary work.
  • Both activities are part of the same application.

Pass only an ID when:

  • The object is large or contains nested collections.
  • The destination can retrieve current data from a repository.
  • The data may change between screens.
  • The destination must recover reliably after process recreation.

For example:

val intent = Intent(this, DetailsActivity::class.java).apply {
    putExtra("user_id", user.id)
}

The destination can then load the latest user information through its ViewModel or repository.

Android recommends keeping Intent data to a few kilobytes. The Binder transaction buffer is limited and shared by transactions in the process. Oversized extras can result in TransactionTooLargeException.

What About Dart and Henson?

Dart and Henson were created to generate Intent bindings and builders through annotations:

  • Dart injected Intent extras into fields in the destination activity.
  • Henson generated builder-style APIs for constructing Intents.

They addressed a genuine Android development problem when Java, manual Parcelable implementations, Support Library imports, and annotation-processing tools were common.

However, the original 2.0.2 setup is now outdated:

compile 'com.f2prateek.dart:dart:2.0.2'
annotationProcessor 'com.f2prateek.dart:dart-processor:2.0.2'

The compile Gradle configuration has been removed, Android projects have moved from the Support Library to AndroidX, Butter Knife is deprecated, and the Dart/Henson project has not kept pace with the current Kotlin-first Android ecosystem.

Existing applications that already depend on these libraries may continue maintaining them temporarily. For new development, a Kotlin Intent contract, @Parcelize, Jetpack Navigation Safe Args, or type-safe Navigation routes are better-supported options.

Intent Contracts vs Navigation Safe Args

The correct approach depends on the application’s navigation structure.

ApproachBest suited for
Intent contractPassing data directly between activities
Navigation Safe ArgsFragment destinations using an XML navigation graph
Type-safe routesKotlin or Jetpack Compose navigation
ViewModel or repositorySharing or reloading larger application data
Activity Result APILaunching another activity and receiving a result
Intent contract
Best suited for
Passing data directly between activities
1 of 5

Safe Args generates type-safe argument classes for destinations in a Navigation graph. It is useful for Fragment-based navigation, but it does not replace explicit Intents when one activity directly launches another.

Automate Your Android Workflow

We build Android Studio plugins that cut manual effort, improve speed, and make your development process seamless.

Common Mistakes When Passing Intent Data

Repeating raw keys

putExtra("user", user)

A misspelling elsewhere is not detected at compile time. Keep keys private inside a contract.

Assuming an extra is present

Avoid force-unwrapping an optional value:

val source = intent.getStringExtra("source")!!

Validate required data explicitly and keep genuinely optional data nullable.

Passing large objects

Images, long lists, database results, and complete API responses should not be placed in an Intent. Store them elsewhere and pass a compact identifier or URI.

Using implicit Intents for internal navigation

Use an explicit Intent when opening a known activity:

Intent(this, DetailsActivity::class.java)

An implicit Intent can be intercepted by another application if sensitive data is included and the target is not constrained.

Confusing Intent and Bundle methods

Intent provides overloaded putExtra() methods. Methods such as putInt(), putString(), and putParcelable() are primarily Bundle methods:

val extras = Bundle().apply {
    putInt("user_id", 42)
    putString("source", "login")
}

val intent = Intent(this, DetailsActivity::class.java).apply {
    putExtras(extras)
}

Both approaches work, but using a contract is more important than choosing between individual extras and an explicit Bundle.

Frequently Asked Questions

How do you pass data between activities in Android?

Create an explicit Intent, add small values with putExtra(), and retrieve them in the destination. A reusable Intent contract keeps keys private and validates required arguments centrally.

How do you pass an object between Android activities?

Make the Kotlin data class implement Parcelable using @Parcelize, pass it through putExtra(), and retrieve it with the typed IntentCompat.getParcelableExtra() method in the destination.

Is Parcelable better than Serializable for Intent extras?

Parcelable is designed for Android component communication and is generally the preferred option. Kotlin’s @Parcelize plugin also generates the required implementation, removing most of Parcelable’s traditional boilerplate.

How much data can an Intent carry?

Keep Intent extras to a few kilobytes. Large payloads share Android’s limited Binder transaction buffer and may cause TransactionTooLargeException. Pass an ID, file URI, or repository key instead.

Not for new projects. They solved Intent boilerplate through generated code, but modern Kotlin projects have better-supported options, including @Parcelize, Intent contracts, Safe Args, and type-safe navigation routes.

How can Intent extras be made type-safe?

Centralize Intent creation and parsing in a contract with typed parameters. For Navigation component destinations, use Safe Args or type-safe routes to obtain generated or compiler-checked argument handling.

Author-Murtuza Kutub
Murtuza Kutub
LinkedIn

A product development and growth expert, helping founders and startups build and grow their products at lightning speed with a track record of success. Apart from work, I love to Network & Travel.

Share this article

Phone

Next for you

8 Best GraphQL Libraries for Node.js in 2025 Cover

Technology

Aug 4, 202613 min read

8 Best GraphQL Libraries for Node.js in 2025

8 Best GraphQL Libraries for Node.js in 2026 Too Long? Read This First - Choose Apollo Server when you need a mature ecosystem, GraphOS integration, plugins, or Apollo Federation. - Choose GraphQL Yoga for a modern, portable server with Fetch API compatibility and built-in support for subscriptions over Server-Sent Events. - Choose Mercurius when your application already uses Fastify and runtime efficiency is a major priority. - Use GraphQL.js when you need the official JavaScript implementati

9 React Native Animation Libraries and Tools Compared Cover

Technology

Aug 4, 202615 min read

9 React Native Animation Libraries and Tools Compared

Too Long? Read This First - Use React Native Reanimated for gesture-driven, interruptible, and performance-sensitive interface animations. - Use the built-in Animated API for simple fades, transforms, and timed sequences without another dependency. - Pair React Native Gesture Handler with Reanimated for swipes, dragging, pinching, rotation, and other touch-driven experiences. - Use Lottie React Native for non-interactive motion graphics supplied by designers. - Choose React Native Skia for cust

9 Critical Practices for Secure Web Application Development Cover

Technology

Aug 4, 202616 min read

9 Critical Practices for Secure Web Application Development

Too Long? Read This First - Define security requirements and model threats before implementation begins. - Treat authentication, account recovery, and MFA as one complete identity system. - Apply server-side authorization to every protected action and object. - Prevent injection with parameterized APIs, structured validation, safe output handling, and restricted outbound requests. - Protect sessions and tokens throughout their complete lifecycle. - Minimise sensitive data and manage encryption