はじめに (対象読者・この記事でわかること)

この記事は、AndroidアプリをJavaで開発している中級者の方を対象にしています。画面遷移時に値を渡したいけど、Intent#putExtra() の挙動がイマイチ掴めない、Fragmentで setArguments() したはいいものの回収できない、といった悩みを抱えている方に最適です。

読み進めることで、Bundle の内部構造とメモリ上の動き、正しい書き込み・読み出しの順番、型安全に値を受け取るための定型パターン、そしてメモリリークを避けるための注意点が身につきます。サンプルコードは Android 14 向けにビルドして動作確認済みですので、すぐにプロジェクトへ移植できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Java の基本的な文法(匿名クラス、ラムダは使わない範囲で解説) - Android Studio でのプロジェクト作成とビルド手順 - Activity や Fragment のライフサイクル基礎知識

Bundle がなぜ必要なのか:画面間データ受渡しの仕組み

Android では、メモリ効率とプロセス隔離のため、各コンポーネント(Activity/Fragment/Service)が別々のメモリ空間で動作します。そのため「変数を static にしておけばOK」という安易な方法は、プロセスキル時にデータが吹き飛び、しかもメモリリークの温床になります。

Bundle は IPC(プロセス間通信)に使える「軽量なシリアライズコンテナ」として設計されており、IntentsavedInstanceState とセットで使うことで、OS によるメモリ管理に耐えられる形でデータを保持できます。内部的には ArrayMap<String, Object> をベースに、プリミティブと ParcelableSerializable のみを受け付ける仕組みになっており、大きなオブジェクトをそのまま放り込むと TransactionTooLargeException が発生します。Bundle を正しく扱うことは、Android アプリの品質とパフォーマンスを左右する重要なポイントです。

実践:安全に Bundle を読み書きする

ステップ1:値を詰める側の実装

まず、次のルールを守ります。 1. Key は private static final String で定義し、重複を避ける 2. 大きなリストは別途 DB/キャッシュに逃し、ID だけを入れる 3. カスタムクラスは Parcelable を実装する(Serializable は速度が劣る)

コード例:

Java
public final class UserDetailActivity extends AppCompatActivity { private static final String EXTRA_USER_ID = "extra_user_id"; private static final String EXTRA_USER_NAME = "extra_user_name"; public static Intent createIntent(@NonNull Context ctx, long userId, @NonNull String userName) { Intent intent = new Intent(ctx, UserDetailActivity.class); intent.putExtra(EXTRA_USER_ID, userId); intent.putExtra(EXTRA_USER_NAME, userName); return intent; } ... }

ファクトリメソッドを用意することで、Bundle のキーがクラス外に漏れることを防げます。

ステップ2:値を取り出す側の実装

onCreate() 内で getIntent().getExtras() を呼び、デフォルト値を明示します。Null 安全にするため、Intent#get*Extra() のオーバーロードを使うか、AndroidX の IntentCompat を活用しましょう。

Java
Bundle extras = getIntent().getExtras(); if (extras == null) { finish(); return; } long userId = extras.getLong(EXTRA_USER_ID, -1L); String name = extras.getString(EXTRA_USER_NAME); if (userId == -1L || name == null) { finish(); return; }

Fragment の場合は setArguments(Bundle) したあと、getArguments() で受け取りますが、Fragment が再作成されたタイミングで OS によって Bundle が復元されるため、コンストラクタ経由で値を渡すと消失します。

ハマった点:TransactionTooLargeException と原因調査

開発中、画像のバイト配列を Bitmap→ByteArray→Intent へそのまま詰め込んでいたところ、ギャラリーから大きめ画像を選ぶとクラッシュ。Logcat には !!! FAILED BINDER TRANSACTION !!! が。これは Binder トランザクションの上限(約 1 MB)を超えたためです。

解決策:URI と ViewModel での遅延ロード

  1. 画像は一時ファイル or MediaStore URI に保存
  2. Intent には URI 文字列のみを入れる
  3. 受け側の ViewModel でバックグラウンドスレッドから画像をロードし、LiveData で UI へ通知
Java
// 送り側 File tempFile = File.createTempFile("img", ".jpg", getCacheDir()); Uri uri = FileProvider.getUriForFile(this, AUTH, tempFile); intent.putExtra(EXTRA_IMAGE_URI, uri.toString()); // 受け側 String uriStr = getIntent().getStringExtra(EXTRA_IMAGE_URI); Uri uri = Uri.parse(uriStr); imageViewModel.load(uri); // 内部で Glide または Coil

これで Binder サイズを気にせず、かつ設定変更(画面回転)にも対応できます。

まとめ

本記事では、Android の Bundle を使った画面間データ受渡しの仕組みと、メモリ効率・型安全性を両立させる実装パターンを解説しました。

  • Bundle は IPC 向けシリアライズコンテナであり、大きなデータは入れない
  • Key は private 定数で管理し、ファクトリメソッドで隠蔽する
  • 画像のような大きなバイト列は URI に逃し、ViewModel で遅延ロードする

この記事を通して、クラッシュしない・メモリリークしない、保守しやすい Android アプリの実装が身についたはずです。 次回は Jetpack Navigation Component と SavedStateHandle を組み合わせた、よりモダンで安全な方法について掘り下げます。

参考資料