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

- 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
) : ParcelableParcelable 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.
| Approach | Best suited for |
| Intent contract | Passing data directly between activities |
| Navigation Safe Args | Fragment destinations using an XML navigation graph |
| Type-safe routes | Kotlin or Jetpack Compose navigation |
| ViewModel or repository | Sharing or reloading larger application data |
| Activity Result API | Launching another activity and receiving a result |
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.
Are Dart and Henson still recommended for Android?
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.



