Shuttle について
コンポーネント間で大きな Serializable オブジェクトを安全に受け渡すための Android フレームワークです。オブジェクトを保存して小さな識別子だけを渡すことで、TransactionTooLargeException によるクラッシュを防ぎます。本番アプリを開発する Android 開発者向けです。
主な機能
- 01大きなオブジェクトを保存し、小さな識別子で受け渡し
- 02Intent と Navigation Component による転送
- 03LCE パターンに従ったシールドクラスの結果
- 04@ShuttleCargo アノテーションと KSP によるコード生成
- 05Room ベースの既定の永続化実装
活用シーン
- 01Activity や Fragment 間で大きな画像モデルを受け渡す
- 02肥大化した Bundle による Binder トランザクションのクラッシュを避ける
- 03画面に戻った際に使用済みのデータを自動で削除する
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
docker run -i --rm ghcr.io/grarcht/shuttle-mcp:4.0.0ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ghcr.io/grarcht/shuttle-mcp:4.0.0stdioツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
TransactionTooLargeException によるクラッシュを、確実に防ぎます。
大きな Serializable オブジェクトをコンポーネント間で安全に受け渡し、クラッシュを防ぐ最新の Android フレームワークです。
すぐに始める
🚨 Shuttle を選ぶ理由
🗞️ **Android Weekly #594 と Android Weekly #455 で特集され、**Android コミュニティによって 2 度検証されました。
きっと見覚えがあるはずです。もしかすると先週のことかもしれません。
android.os.TransactionTooLargeException: data parcel size X bytes
開発環境では発生しませんでした。QA でも発生しませんでした。午前 2 時、本番環境で実際のユーザーに発生しました。チームの誰も気づく前に、Play ストアの評価に影響が出ていました。
そこで、問題を切り分け、チケットを作成し、修正を書き、PR をレビューし、QA を再度実施し、ホットフィックスをリリースしました。そして、次のエンジニアが同じことが再び起きる前に気づくよう、コードレビューのチェックリストに追加しました。
それでも、確実には防げません。構造的な問題は、レビューを重ねても解決できません。
Shuttle は、アプリのクラッシュを防ぐために、大きな Serializable オブジェクトを Intent オブジェクトとともに渡したり、Bundle オブジェクトに保存したりするための、最新の安全な方法を提供します。このクラッシュは、ガバナンスによって防ぐのではなく、構造的に発生を防止します。
コードレビューによるガバナンスに、これ以上時間と費用をかけ続ける必要があるでしょうか。問題を解決する方法を提供して、根本から取り組んでみませんか。
Shuttle は、TransactionTooLargeException を引き起こすコードを検出するために必要な高度なガバナンスを、次の方法で軽減します。
Serializableを保存し、そのSerializableの識別子を渡す- バインダートランザクションに小さなサイズの
Bundleを使用する TransactionTooLargeExceptionによるアプリのクラッシュを回避する- 保存した
Serializableを宛先で取得できるようにする
Shuttle には、さらに次の特長があります。
- Maven アーティファクトによるソリューションを提供する
- 構築の基盤となる Solution Building Blocks(SBB)を提供する
- DB やテーブルのセットアップが不要なため、時間を節約できる。特に、異なる種類のオブジェクトのコンテンツ用に多数のテーブルを作成する場合に有効
アーキテクチャを構想、設計、作成する際には、品質属性とベストプラクティスを念頭に置きました。これらの属性には、ユーザビリティ、可読性、認識しやすさ、再利用性、保守性などが含まれます。
| Shuttle なし | Shuttle あり |
|---|---|
大きな Serializable を Intent/Bundle で渡す | オブジェクトを倉庫に保存し、小さな識別子だけを渡す |
| 開発環境では発生せず、本番環境では致命的 | どこでもバインダートランザクションが安全なサイズ制限内に収まる |
| クラッシュの調査、修正、QA、ホットフィックスに時間と費用がかかる | クラッシュの種類を構造的に発生不可能にする |
| 継続的なコードレビューによるガバナンスが必要 | 安心してリリースできる |
| エンジニアがオブジェクトのライフサイクルを手動で管理する | 貨物の自動またはオンデマンドのクリーンアップを標準搭載 |
![]() | ![]() |
⚙️ 仕組み
Shuttleフレームワークの名前は、貨物業界の貨物輸送に由来しています。引っ越し業者や保管業者では、大型の引っ越しトラックで目的地(倉庫、住宅など)まで貨物をすべて運べない状況が発生します。こうした状況は、道路の規制や、大量の貨物によるトラックの積載超過などが原因で起こります。そのため、業者は小型のShuttleバンを使い、小分けにした貨物を複数回に分けて輸送し、すべての荷物を届けます。
配送が完了した後、従業員はShuttleバンやトラックから残った貨物を降ろします。この片付け作業は、仕事の最後の手順のひとつです。
Shuttleフレームワークは、こうした状況をもとにしています。
- データを目的地へ正常に届けるための、小さな貨物バンドルオブジェクトを作成する
- 対応する大きな貨物を倉庫へ運び、受け取りまで保管する
- 識別子を使って、小さな貨物と大きな貨物を紐付ける
- 貨物の輸送に使用する唯一の信頼できる情報源(Shuttleインターフェース)を提供する
- 貨物を削除するための便利な関数を提供する(自動またはオンデマンド)
Shuttleは、Androidのバインダートランザクション制限に同じロジックを適用します。
┌─────────────────────────────────────────────────────────────┐
│ Source Component │
│ 1. Large Serializable -> stored in Warehouse (Room/DB) │
│ 2. Small cargo ID -> passed in Intent/Bundle │
└──────────────────────────────┬──────────────────────────────┘
│ (tiny binder transaction)
┌──────────────────────────────▼──────────────────────────────┐
│ Destination Component │
│ 3. Cargo ID received -> retrieved from Warehouse │
│ 4. Large Serializable -> delivered via Kotlin Channel │
│ 5. Cleanup -> cargo removed from Warehouse automatically │
└─────────────────────────────────────────────────────────────┘
要件
| 要件 | 最小要件 |
|---|---|
| Android min SDK | 26 |
| Kotlin | 2.2.10 |
| AGP(Android Gradle Plugin) | 8.0+ |
| KSP | @ShuttleCargoの使用時のみ必須 |
| Java | 21 |
🚀 クイックスタート
1. 依存関係を追加する
settings.gradle.kts — Shuttle Gradleプラグインを適用して、@ShuttleCargoアノテーションプロセッサをビルドに組み込みます。
@ShuttleCargoは、画面間で受け渡す貨物クラスに付与します。ビルド時にアノテーションプロセッサが、手動でSerializableの定型コードを記述しなくても、Shuttleが貨物を保存および取得するために必要なシリアル化コードを生成します。
pluginManagement {
plugins {
id("com.grarcht.shuttle.cargo") version "4.0.0"
}
}
Shuttle Cargo GradleプラグインはKSPを設定し、Shuttleコンパイラプラグインを自動的に登録します。このプラグインがないと、@ShuttleCargoを付与したクラスに必要なシリアル化用の連携コードが生成されません。
build.gradle.kts:
plugins {
id("com.grarcht.shuttle.cargo")
}
dependencies {
implementation(platform("com.grarcht.shuttle:shuttle-bom:4.0.0"))
implementation("com.grarcht.shuttle:framework")
implementation("com.grarcht.shuttle:framework-integrations-persistence")
implementation("com.grarcht.shuttle:framework-integrations-extensions-room")
implementation("com.grarcht.shuttle:framework-addons-navigation-component") // Optional
// Annotation-based API — use @ShuttleCargo to own the serialization contract
implementation("com.grarcht.shuttle:framework-annotations")
ksp("com.grarcht.shuttle:framework-annotations-processor")
}
バージョンカタログ(libs.versions.toml):
[versions]
shuttle = "4.0.0"
[plugins]
shuttle-cargo = { id = "com.grarcht.shuttle.cargo", version.ref = "shuttle" }
[libraries]
shuttle-bom = { group = "com.grarcht.shuttle", name = "shuttle-bom", version.ref = "shuttle" }
shuttle-framework = { group = "com.grarcht.shuttle", name = "framework" }
shuttle-persistence = { group = "com.grarcht.shuttle", name = "framework-integrations-persistence" }
shuttle-room = { group = "com.grarcht.shuttle", name = "framework-integrations-extensions-room" }
shuttle-navigation = { group = "com.grarcht.shuttle", name = "framework-addons-navigation-component" }
shuttle-annotations = { group = "com.grarcht.shuttle", name = "framework-annotations" }
shuttle-annotations-processor = { group = "com.grarcht.shuttle", name = "framework-annotations-processor" }
2. Shuttleを初期化する
CargoShuttleをシングルトンとして一度だけ設定します。Hiltを使用する場合:
@Provides @Singleton
fun provideShuttle(facade: ShuttleFacade, warehouse: ShuttleWarehouse): Shuttle =
CargoShuttle(facade, warehouse)
@Provides @Singleton
fun provideShuttleFacade(
@ApplicationContext context: Context,
warehouse: ShuttleWarehouse
): ShuttleFacade = ShuttleCargoFacade(context as Application, warehouse)
@Provides @Singleton
fun provideShuttleWarehouse(
dao: ShuttleDataAccessObject,
factory: ShuttleDataModelFactory,
@ApplicationContext context: Context,
gateway: ShuttleFileSystemGateway
): ShuttleWarehouse = ShuttleRepository(dao, factory, context.filesDir.absolutePath, gateway)
ShuttleDataAccessObjectは、ShuttleRoomDataDb.getInstance(ShuttleRoomDbConfig(context)).shuttleDataAccessObjectから取得します。データを受け渡す箇所でShuttleをインジェクトします。
3. 最初の貨物を送る
// Source: transport a large Serializable via Intent
shuttle.intentCargoWith(context, DestinationActivity::class.java)
.transport(cargoId, myLargeSerializable)
.cleanShuttleOnReturnTo(SourceFragment::class.java, DestinationActivity::class.java, cargoId)
.deliver(context)
// Destination: pick up the cargo
lifecycleScope.launch {
getShuttleChannel()
.consumeAsFlow()
.collectLatest { result ->
when (result) {
is ShuttlePickupCargoResult.Success<*> -> render(result.data as MyModel)
is ShuttlePickupCargoResult.Error<*> -> showError()
ShuttlePickupCargoResult.Loading -> showLoading()
}
}
}
これだけです。カスタムDBの設定も、テーブル管理も不要です。クラッシュも発生しません。
📦 使用方法
推奨されるエントリーポイントはShuttleインターフェースで、実装にはCargoShuttleを使用します。すべての貨物輸送操作における唯一の信頼できる情報源です。
Intentで受け渡す
送信元コンポーネント:
val cargoId = ImageMessageType.ImageData.value
shuttle.intentCargoWith(context, MVCSecondControllerActivity::class.java)
.transport(cargoId, imageModel)
.cleanShuttleOnReturnTo(
MVCFirstControllerFragment::class.java,
MVCSecondControllerActivity::class.java,
cargoId
)
.deliver(context)
ℹ️
cleanShuttleOnReturnToは重要です。不要になった貨物がWarehouseから確実に削除されます。
Navigation Componentで受け渡す
送信元フラグメント:
val cargoId = ImageMessageType.ImageData.value
navController.navigateWithShuttle(shuttle, R.id.MVVMNavSecondViewActivity)
?.logTag(LOG_TAG)
?.transport(cargoId, imageModel as Serializable)
?.cleanShuttleOnReturnTo(
MVVMNavFirstViewFragment::class.java,
MVVMNavSecondViewActivity::class.java,
cargoId
)
?.deliver()
目的地で貨物を受け取る
Fragment/Activity内:
lifecycleScope.launch {
getShuttleChannel()
.consumeAsFlow()
.collectLatest { result ->
when (result) {
ShuttlePickupCargoResult.Loading -> initLoadingView(view)
is ShuttlePickupCargoResult.Success<*> -> { showSuccessView(view, result.data as ImageModel); cancel() }
is ShuttlePickupCargoResult.Error<*> -> { showErrorView(view); cancel() }
}
}
}
ViewModel内:
viewModelScope.launch {
shuttle.pickupCargo<Serializable>(cargoId = cargoId)
.consumeAsFlow()
.collectLatest { result ->
pickupCargoMutableStateFlow.value = result
when (result) {
is ShuttlePickupCargoResult.Success<*>,
is ShuttlePickupCargoResult.Error<*> -> cancel()
else -> { /* await */ }
}
}
}
貨物の状態(LCEパターン)
Shuttleは、**Loading-Content-Error(LCE)**パターンを促すsealed classの結果を返すため、利用側でUIの状態、分析、エラー処理を完全に制御できます。
| 操作 | 戻り値の型 | 状態 |
|---|---|---|
| 貨物の保存 | Channel<ShuttleStoreCargoResult> | Storing、Success、Error |
| 貨物の受け取り | Channel<ShuttlePickupCargoResult> | Loading、Success、Error |
| 貨物の削除 | Channel<ShuttleRemoveCargoResult> | Removing、Success、Error |
@ShuttleCargo の使用
Shuttle を介して転送したいデータクラスにアノテーションを付けます。アノテーションプロセッサーがビルド時にシリアライズコードを生成するため、手動で記述する必要はありません。
@ShuttleCargo
data class ImageModel(
val id: String,
val title: String,
val byteArray: ByteArray
)
宣言はこれだけです。Serializable の実装も、カスタムの読み取り/書き込みメソッドも必要ありません。その後は、ほかのオブジェクトと同じように転送できます。
shuttle.intentCargoWith(context, DestinationActivity::class.java)
.transport(cargoId, imageModel)
.deliver(context)
クリーンアップ
cleanShuttleOnReturnTo を使用すると、Cargo は自動的に削除されます。手動で制御する場合は、次のようにします。
// Remove a specific cargo item
shuttle.removeCargoBy(cargoId)
// Remove all cargo
shuttle.removeAllCargo()
🏗️ アーキテクチャ
Shuttle は、階層化された ソリューション構築ブロック(SBB) フレームワークです。各レイヤーの責務は明確に定義されており、どのレイヤーも利用者に技術の選択を強制しません。
モジュールの概要
| モジュール | 役割 | 必須? |
|---|---|---|
framework | コアインターフェース、転送ロジック、封印された結果型 | ✅ はい |
framework-integrations-persistence | 永続化の抽象化/インターフェース | ✅ はい |
framework-integrations-extensions-room | 永続化インターフェースの Room 実装 | ⚡ デフォルト(差し替え可能) |
framework-addons-navigation-component | Navigation Component との統合 | ➕ 任意 |
framework-annotations | データクラスを転送可能としてマークする @ShuttleCargo アノテーション | ➕ 任意 |
framework-annotations-processor | ビルド時にシリアライズコードを生成する KSP プロセッサー | ➕ 任意 |
framework-annotations-gradle-plugin | KSP とコンパイラープラグインを自動的に連携させる Gradle プラグイン | ➕ 任意 |
コンテキスト図

モジュール依存関係図
graph TD
A[Your Application]
A --> B[framework / core]
A --> C[framework-addons-navigation-component]
A --> D[your custom integration]
B --> E[framework-integrations-persistence / abstraction layer]
E --> F[framework-integrations-extensions-room / default implementation]
この階層化が重要な理由: 永続化の抽象化により、フレームワークやアプリケーションコードに触れることなく、Room をほかのストレージ実装に置き換えられます。統合インターフェースを実装すれば、独自の永続化レイヤーを導入できます。
Shuttle は大規模なリアクティブライブラリを同梱しません。非同期通信には Kotlin Coroutines と Channels のみを使用するため、推移的な依存関係を最小限に抑えられます。
🎬 デモアプリ
デモアプリでは、画像データの転送を例に、クラッシュが発生するケースと Shuttle による解決策を並べて紹介します。画像データは、実際の環境で TransactionTooLargeException を引き起こす最も一般的な要因の 1 つです。
次の 2 つのアーキテクチャパターンを扱います。
MVVM: Activity/Fragment を View とし、ViewModel が状態の所有者および連携役を担い、非同期通知には Kotlin Channels を使用します。
フロー 1:Shuttle で遷移 ✅
「Shuttle を使用して遷移」 をタップすると、倉庫からの受け取りを通じて画像が正常に読み込まれます。
| メインメニュー | Cargo の受け取り完了 |
|---|---|
![]() | ![]() |
フロー 2:通常どおり遷移 ❌
「通常どおり遷移」 をタップすると、アプリが TransactionTooLargeException でクラッシュします。
| メインメニュー | クラッシュ後 |
|---|---|
![]() | ![]() |
ℹ️ 本番環境で画像を読み込む場合は、Glide または Coil を使用してください。このデモでは、クラッシュが発生する状況を意図的に再現するため、未加工の画像データを使用しています。
⚠️ 注意:トレードオフを理解しましょう
同じ Intent 内のほかの Parcelable オブジェクトが原因で、アプリがクラッシュする可能性は残ります。 Shuttle が保護するのは Serializable ペイロードです。一緒に渡す無関係な Parcelable データは保護しません。
Serializable は Parcelable より遅くなります。 Parcelable は IPC 向けに最適化されており、読み込みも高速ですが、ディスクへの保存には適していません。Google は永続化にはシリアライゼーションを推奨しているため、Shuttle ではこれを採用しています。LCE 状態パターン(読み込み状態)を使えば、UI は少し長くなる読み込み時間に適切に対応できます。
これらは文書化されたトレードオフであり、バグではありません。アーキテクチャとは、常に選択肢を比較検討することです。この選択には価値があります。
🤖 AI
Model Context Protocol (MCP)
Model Context Protocol は、AI アシスタントが外部のツールやデータソースに接続できるオープン標準です。トレーニングデータだけに頼るのではなく、MCP 対応アシスタントは必要なときにリアルタイムのツールを呼び出し、正確な定型コードを生成したり、実際のコードを分析したり、エディターを離れることなく最新のドキュメントを返したりできます。
Shuttle は MCP サーバーを MCP Registry に公開しています。つまり、開発者は誰でも、MCP 対応エディターを接続するだけで、このリポジトリをクローンしたり開いたりせずに、自分のプロジェクト内で Shuttle に関する専門知識を活用できます。AI アシスタントは、Hilt、Koin、手動配線向けのすぐに貼り付けられる Kotlin 定型コードを使って Shuttle の統合全体をスキャフォールドしたり、貼り付けた Kotlin または Java のスニペットから TransactionTooLargeException のリスクを検出したり、コアコンセプト、セットアップ、トランスポート、受け取り、クリーンアップ、アノテーションを網羅した公式の Shuttle 仕様を必要に応じて取得したりできます。
チームにとって、これはコードレビューだけでは実現できないガバナンスの強化につながります。経験レベルやエディターの好みに関係なく、すべての開発者が初日から、統合パターンとリスクについて同じ正確かつ最新のガイダンスを得られます。
サポート対象エディターの全リストと検証チェックリストについては、MCP README ファイルをご覧ください。
🤝 貢献
プルリクエストを歓迎します。プルリクエストを作成する前に、貢献ガイドと行動規範をご確認ください。
- リポジトリをフォークします
- フィーチャーブランチを作成します:
git checkout -b feature/my-feature - 変更をコミットします:
git commit -m 'Add: my feature' - ブランチをプッシュします:
git push origin feature/my-feature - プルリクエストを作成します
バグ報告や機能リクエストは、イシューを作成してください。
📄 ライセンス
MIT。ライセンスの全文は LICENSE.mdをご覧ください。
Copyright © 2023 Craft & Graft LLC · GRARCHT ™ 2021
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。




