Table of Contents
Why Pass Data Between Activities
In a real app, screens rarely work in isolation. A list screen might need to tell a detail screen which item to show. A login screen needs to pass the username to a welcome screen. Passing data between activities connects these screens so the user experience feels continuous.
In Android, activities do not directly call each other like functions. Instead, they communicate through Intent objects. For data, you attach extra information to the Intent that starts the next activity. Understanding this pattern is essential for any nontrivial app.
Data as Intent Extras
When you navigate from one activity to another using an Intent, you can attach additional values to that Intent. These are called "extras." The sending activity adds extras before starting the new activity, and the receiving activity reads those extras when it starts.
The core rule is:
Always use the same key, type, and access method for a value in both activities when passing data via intent extras.
An extra is identified by a String key. You use this key to store the data in one activity and to retrieve it in the other.
Sending Simple Data Types
To pass data, you first create an Intent and then attach extras with methods like putExtra. There are many overloaded putExtra functions for different types, such as String, Int, Boolean, and others.
Here is an example from a MainActivity that opens DetailsActivity and passes a username and a user ID:
val intent = Intent(this, DetailsActivity::class.java)
intent.putExtra("EXTRA_USERNAME", "alice")
intent.putExtra("EXTRA_USER_ID", 42)
startActivity(intent)
The keys "EXTRA_USERNAME" and "EXTRA_USER_ID" are arbitrary strings, but they must be unique within the intent and must match exactly when you use them in the receiving activity.
It is common to define keys as const val to avoid typos. For example, in MainActivity:
companion object {
const val EXTRA_USERNAME = "com.example.myapp.USERNAME"
const val EXTRA_USER_ID = "com.example.myapp.USER_ID"
}Then use these constants instead of raw strings:
val intent = Intent(this, DetailsActivity::class.java)
intent.putExtra(EXTRA_USERNAME, "alice")
intent.putExtra(EXTRA_USER_ID, 42)
startActivity(intent)This makes the code more maintainable and less error prone as the app grows.
Receiving Data in the Target Activity
The receiving activity reads extras from its Intent. A typical place to do this is in onCreate, after calling super.onCreate.
In DetailsActivity, to read the values sent above:
class DetailsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details)
val username = intent.getStringExtra(MainActivity.EXTRA_USERNAME)
val userId = intent.getIntExtra(MainActivity.EXTRA_USER_ID, -1)
// Use username and userId, for example set them to TextViews
}
}
For primitive and numeric types, you must provide a default value when using methods like getIntExtra. This default is used if the key was not found. For reference types like String, if the key is missing, the result will be null.
The main reading methods include getStringExtra, getIntExtra, getBooleanExtra, getLongExtra, and others. Each one expects the same type that you passed with putExtra.
Nullable Results and Safe Access
When reading extras, you often deal with nullable values. For String, getStringExtra returns a nullable String. You need to decide how to handle the case where the data is not present.
A simple pattern is to provide a fallback:
val username = intent.getStringExtra(MainActivity.EXTRA_USERNAME) ?: "Guest"Or you might want to fail early if required data is missing:
val username = intent.getStringExtra(MainActivity.EXTRA_USERNAME)
?: throw IllegalStateException("Username extra is required")
For numeric values, the default parameter in methods like getIntExtra acts as your fallback. Choose a default that clearly indicates an invalid or missing value, for example -1 for IDs that are always non negative.
Do not assume extras are always present. Always handle nullability or provide safe defaults when reading intent extras.
Passing Collections and Arrays
You can also pass arrays and some collections as extras. For example, to pass a list of selected item IDs:
val selectedIds = intArrayOf(5, 8, 13)
val intent = Intent(this, SummaryActivity::class.java)
intent.putExtra("EXTRA_SELECTED_IDS", selectedIds)
startActivity(intent)
In SummaryActivity:
val selectedIds = intent.getIntArrayExtra("EXTRA_SELECTED_IDS") ?: intArrayOf()
Similar methods exist for other types, such as getStringArrayExtra. For general lists, you typically use ArrayList with types that implement Parcelable or Serializable, which will be covered when you pass complex objects.
Passing Objects with Parcelable
When you need to pass more structured data than basic types, such as a full user profile or a product model, you cannot directly pass arbitrary Kotlin objects as extras. Instead, the object must be converted into a format that the Android system can send between components.
Android provides two main interfaces for this: Parcelable and Serializable. In Android apps, Parcelable is preferred because it is more efficient and specifically designed for Android.
To use Parcelable in Kotlin, you usually take advantage of the Kotlin Android extensions that generate boilerplate for you. In modern code, you declare a data class and annotate it with @Parcelize.
Here is an example of a User model that can be passed between activities:
@Parcelize
data class User(
val id: Int,
val name: String,
val email: String
) : ParcelableIn the sending activity:
val user = User(1, "Alice", "alice@example.com")
val intent = Intent(this, ProfileActivity::class.java)
intent.putExtra("EXTRA_USER", user)
startActivity(intent)
In ProfileActivity, to read the object:
val user = intent.getParcelableExtra<User>("EXTRA_USER")Once again, the key must match exactly. The returned value is nullable, so you handle it in the same way as other extras.
Only pass Parcelable or Serializable objects as extras, never arbitrary classes that do not implement a supported interface.
Constraints on What You Can Pass
Not every object is safe or allowed to be passed between activities. Some data, such as large bitmaps, open file streams, or database connections, should not be added as extras. The intent extras need to be serialized, so passing very large or complex data can cause performance problems or even errors.
There is also a size limit on the data you can pass through an Intent. While the exact limit is not specified in a simple number for every case, trying to pass very large extras can result in a TransactionTooLargeException. As a rule of thumb, keep extras relatively small and use other storage options for large data.
A short guideline is that extras should mostly contain identifiers, flags, and small objects. Larger data, such as big lists or media files, should be accessed by reference, for example through a database query or a URI, instead of being passed directly.
Passing URIs and References Instead of Large Data
Instead of passing big data itself, a common pattern is to pass a reference to that data. For example, if you have an image file stored in internal storage, pass a Uri or a file path, and let the receiving activity load the actual file.
For example, in the sending activity:
val imageUri: Uri = // obtain a Uri pointing to your image
val intent = Intent(this, ImageViewerActivity::class.java)
intent.putExtra("EXTRA_IMAGE_URI", imageUri.toString())
startActivity(intent)
And then in ImageViewerActivity:
val imageUriString = intent.getStringExtra("EXTRA_IMAGE_URI")
if (imageUriString != null) {
val imageUri = Uri.parse(imageUriString)
// load the image using the Uri
}This approach keeps your navigation payload small and shifts heavy loading operations into the destination activity.
Centralizing Keys for Reusability
As your project grows, using raw strings for every intent key becomes fragile. It is a good practice to centralize and reuse them. You can keep the keys in the activity that knows the meaning of its inputs, or in a separate file.
For example, in a DetailsActivity, define its own keys:
class DetailsActivity : AppCompatActivity() {
companion object {
const val EXTRA_ITEM_ID = "com.example.myapp.ITEM_ID"
fun createIntent(context: Context, itemId: Long): Intent {
return Intent(context, DetailsActivity::class.java).apply {
putExtra(EXTRA_ITEM_ID, itemId)
}
}
}
// ...
}Then, from any other activity:
val intent = DetailsActivity.createIntent(this, 123L)
startActivity(intent)
This pattern hides the keys and ensures that all callers construct the Intent consistently.
Passing Data and Activity Results
Sometimes the flow between activities is not one way. You might start another activity to let the user choose something, then expect a result back. While the details of starting activities for a result are covered separately, it is useful to understand that the same idea applies in reverse.
The called activity sets a result intent with extras and sends it back. For example, an activity that picks a color might create a result intent, attach the chosen color, and finish. The original activity then reads that data in a callback.
Even in this case, the communication uses intents and extras, just in the opposite direction. The keys and type consistency rules remain exactly the same.
Choosing What to Pass
When designing your navigation, think carefully about what data each activity really needs. A simple rule is that the destination activity should receive enough information to reconstruct its user interface and behavior, but not more.
For example, instead of passing a fully loaded list of products to a detail activity, pass just the product ID. The detail activity can then load the rest of the information from the network or database. This reduces memory usage, avoids duplication, and simplifies the code.
If a piece of data must survive configuration changes or longer lifetimes, consider using scoped storage and architecture components, which are better tools for long lived data than intent extras.
Common Pitfalls and How to Avoid Them
There are a few mistakes that often occur when passing data between activities. One is using mismatched keys, which leads to null values on the receiving side. Using constants or helper methods for intent creation helps avoid this.
Another common issue is assuming that extras are always present and not handling nulls. Activities can be started from multiple places, or the user may navigate in unexpected ways. It is safer to validate inputs and provide sensible defaults.
Passing large or complex objects is also a frequent source of trouble. If you encounter performance problems or exceptions when navigating, check the size of your extras and refactor large data into a more appropriate storage layer.
By keeping your extras small, your keys organized, and your expectations clear, you will have reliable communication between activities that scales as your app becomes more complex.