Passing Data Between Screens
Every non-trivial Android app needs to move information between activities. A user taps a list item and the detail screen must know which item was tapped. A form screen completes and the calling screen must receive the result. Android provides a purpose-built mechanism for all of this: the Intent extras system, the Bundle class for grouping values, and the Activity Result API for two-way communication. This lesson covers all three in depth.
Intent Extras — The Basic Pattern
An Intent carries a extras Bundle — essentially a typed key-value map. You attach values with putExtra() overloads and read them on the receiving end with the matching get*Extra() methods.
// ---- Sending activity ----
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("product_id", 42);
intent.putExtra("product_name", "Wireless Keyboard");
intent.putExtra("in_stock", true);
startActivity(intent);
// ---- Receiving activity (DetailActivity.java) ----
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
Intent intent = getIntent();
int productId = intent.getIntExtra("product_id", -1); // -1 = default
String productName = intent.getStringExtra("product_name");
boolean inStock = intent.getBooleanExtra("in_stock", false);
TextView nameView = findViewById(R.id.textProductName);
nameView.setText(productName);
}
Key naming convention: Define extra keys as public static final String constants on the receiving activity. For example, public static final String EXTRA_PRODUCT_ID = "com.example.shop.EXTRA_PRODUCT_ID";. Using the fully-qualified package prefix prevents key collisions if your app ever receives intents from other apps.
Supported Extra Types
The putExtra() / get*Extra() family supports all Java primitives and their arrays, String, CharSequence, Parcelable, Serializable, and ArrayList variants. The most commonly used are:
int, long, double, float, boolean
String — by far the most common
Parcelable — the efficient Android-native serialization format
Serializable — standard Java serialization; slower than Parcelable, avoid for large objects
Bundles — Grouping Related Values
A Bundle is a standalone key-value container. You can build a Bundle, populate it, then embed it as a single extra. This is the pattern used by Fragments (covered in Lesson 6) and is also how the system saves/restores Activity state.
// Build a Bundle and attach it
Bundle productBundle = new Bundle();
productBundle.putInt("id", 42);
productBundle.putString("name", "Wireless Keyboard");
productBundle.putDouble("price", 29.99);
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("product_data", productBundle);
startActivity(intent);
// Unpack it in DetailActivity
Bundle data = getIntent().getBundleExtra("product_data");
if (data != null) {
int id = data.getInt("id");
String name = data.getString("name");
double price = data.getDouble("price");
}
Prefer Parcelable over Serializable. When your data is a custom object, implement the Parcelable interface (or annotate with @Parcelize in Kotlin). Parcelable uses Android's own binary format — roughly 10× faster than Java serialization and produces less garbage on the heap. Serializable is fine for simple, low-frequency transfers but degrades quickly with large object graphs.
Passing Custom Objects with Parcelable
To pass a Product object directly, implement Parcelable:
import android.os.Parcel;
import android.os.Parcelable;
public class Product implements Parcelable {
private final int id;
private final String name;
private final double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// ---- Parcelable write ----
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
dest.writeDouble(price);
}
// ---- Parcelable read (order must match write) ----
protected Product(Parcel in) {
id = in.readInt();
name = in.readString();
price = in.readDouble();
}
public static final Creator<Product> CREATOR = new Creator<Product>() {
@Override
public Product createFromParcel(Parcel in) { return new Product(in); }
@Override
public Product[] newArray(int size) { return new Product[size]; }
};
@Override
public int describeContents() { return 0; }
// getters …
public int getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
Once the class implements Parcelable, passing it is identical to a primitive:
// Send
intent.putExtra(DetailActivity.EXTRA_PRODUCT, product); // product is Parcelable
// Receive (API 33+ prefers getParcelableExtra with Class; use compat for older)
Product product = (Product) getIntent().getParcelableExtra(EXTRA_PRODUCT);
Do not pass large data sets through Intent extras. There is an OS-level limit on IPC buffer size (roughly 1 MB for the entire transaction). Sending large Bitmaps or long lists as extras will throw a TransactionTooLargeException at runtime. Instead, store large data in a shared ViewModel, a database, or a file and pass only an identifier (ID or URI) through the Intent.
Getting Results Back — The Activity Result API
The older startActivityForResult() / onActivityResult() pair is deprecated. The modern replacement is the Activity Result API introduced in AndroidX Activity 1.2. It decouples registration from launch and works cleanly with the activity lifecycle.
The pattern has three parts:
- Register a launcher as a field, before
onCreate runs.
- Launch using that launcher inside an event handler.
- Handle the result inside the callback you provide at registration time.
// ---- CallerActivity.java ----
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
public class CallerActivity extends AppCompatActivity {
// Step 1: register BEFORE onCreate (as a field initializer)
private final ActivityResultLauncher<Intent> editLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
// Step 3: handle result
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
String updatedName = result.getData().getStringExtra("updated_name");
Toast.makeText(this, "Updated: " + updatedName, Toast.LENGTH_SHORT).show();
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_caller);
Button editButton = findViewById(R.id.btnEdit);
editButton.setOnClickListener(v -> {
// Step 2: launch
Intent intent = new Intent(this, EditActivity.class);
intent.putExtra("current_name", "Wireless Keyboard");
editLauncher.launch(intent);
});
}
}
// ---- EditActivity.java ----
public class EditActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
String currentName = getIntent().getStringExtra("current_name");
EditText nameField = findViewById(R.id.editName);
nameField.setText(currentName);
Button saveButton = findViewById(R.id.btnSave);
saveButton.setOnClickListener(v -> {
String newName = nameField.getText().toString().trim();
Intent resultIntent = new Intent();
resultIntent.putExtra("updated_name", newName);
setResult(RESULT_OK, resultIntent); // RESULT_OK or RESULT_CANCELED
finish(); // closes EditActivity, returns to caller
});
Button cancelButton = findViewById(R.id.btnCancel);
cancelButton.setOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
}
}
Why register before onCreate? registerForActivityResult() must be called before the activity is in the STARTED state. Registering as a field initializer (or in the class body, not inside a click listener) guarantees this. Calling it lazily inside an onClick handler will throw an IllegalStateException.
Result Codes
The result code is a plain int. Android provides two constants: RESULT_OK (-1) and RESULT_CANCELED (0). You can also define custom codes (positive integers) for scenarios where a screen can finish with more than two meaningful outcomes, though two states are usually sufficient if you carry detail in the result Intent extras.
Summary
Use Intent.putExtra() for one-way data delivery — primitives and Strings for simple values, Parcelable objects for complex data, and Bundle when you want to group related values. Always declare extra keys as constants. For two-way communication, register an ActivityResultLauncher before onCreate, launch it from your event handler, and call setResult() + finish() in the responding activity. Keep all transferred data small; pass identifiers, not raw data blobs, when dealing with large payloads.