mcpField
S
Shuttle

Shuttle

ローカル
github.com/grarcht開発ツール8712 週間前に更新

Shuttle について

コンポーネント間で大きな Serializable オブジェクトを安全に受け渡すための Android フレームワークです。オブジェクトを保存して小さな識別子だけを渡すことで、TransactionTooLargeException によるクラッシュを防ぎます。本番アプリを開発する Android 開発者向けです。

主な機能

  1. 01大きなオブジェクトを保存し、小さな識別子で受け渡し
  2. 02Intent と Navigation Component による転送
  3. 03LCE パターンに従ったシールドクラスの結果
  4. 04@ShuttleCargo アノテーションと KSP によるコード生成
  5. 05Room ベースの既定の永続化実装

活用シーン

  1. 01Activity や Fragment 間で大きな画像モデルを受け渡す
  2. 02肥大化した Bundle による Binder トランザクションのクラッシュを避ける
  3. 03画面に戻った際に使用済みのデータを自動で削除する

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"grarcht-shuttle": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/grarcht/shuttle-mcp:4.0.0"
]
}
}
}
起動コマンドdocker run -i --rm ghcr.io/grarcht/shuttle-mcp:4.0.0

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
ocighcr.io/grarcht/shuttle-mcp:4.0.0

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

Shuttle
TransactionTooLargeException によるクラッシュを、確実に防ぎます。

大きな Serializable オブジェクトをコンポーネント間で安全に受け渡し、クラッシュを防ぐ最新の Android フレームワークです。


ライセンス:MIT Maven Central API ドキュメント 最小 SDK Kotlin


Android Weekly 第 594 号 Android Weekly 第 455 号


すぐに始める


🚨 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 を引き起こすコードを検出するために必要な高度なガバナンスを、次の方法で軽減します。

  1. Serializable を保存し、その Serializable の識別子を渡す
  2. バインダートランザクションに小さなサイズの Bundle を使用する
  3. TransactionTooLargeException によるアプリのクラッシュを回避する
  4. 保存した Serializable を宛先で取得できるようにする

Shuttle には、さらに次の特長があります。

  1. Maven アーティファクトによるソリューションを提供する
  2. 構築の基盤となる Solution Building Blocks(SBB)を提供する
  3. DB やテーブルのセットアップが不要なため、時間を節約できる。特に、異なる種類のオブジェクトのコンテンツ用に多数のテーブルを作成する場合に有効

アーキテクチャを構想、設計、作成する際には、品質属性とベストプラクティスを念頭に置きました。これらの属性には、ユーザビリティ、可読性、認識しやすさ、再利用性、保守性などが含まれます。

Shuttle なしShuttle あり
大きな Serializable を Intent/Bundle で渡すオブジェクトを倉庫に保存し、小さな識別子だけを渡す
開発環境では発生せず、本番環境では致命的どこでもバインダートランザクションが安全なサイズ制限内に収まる
クラッシュの調査、修正、QA、ホットフィックスに時間と費用がかかるクラッシュの種類を構造的に発生不可能にする
継続的なコードレビューによるガバナンスが必要安心してリリースできる
エンジニアがオブジェクトのライフサイクルを手動で管理する貨物の自動またはオンデマンドのクリーンアップを標準搭載
README の画像README の画像

⚙️ 仕組み

クラッシュからAndroidアプリを救う | Shuttleフレームワークとソリューション

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 SDK26
Kotlin2.2.10
AGP(Android Gradle Plugin)8.0+
KSP@ShuttleCargoの使用時のみ必須
Java21

🚀 クイックスタート

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-componentNavigation Component との統合➕ 任意
framework-annotationsデータクラスを転送可能としてマークする @ShuttleCargo アノテーション➕ 任意
framework-annotations-processorビルド時にシリアライズコードを生成する KSP プロセッサー➕ 任意
framework-annotations-gradle-pluginKSP とコンパイラープラグインを自動的に連携させる Gradle プラグイン➕ 任意
コンテキスト図

Shuttle コンテキスト図

モジュール依存関係図
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 の受け取り完了
README の画像README の画像
フロー 2:通常どおり遷移 ❌

「通常どおり遷移」 をタップすると、アプリが TransactionTooLargeException でクラッシュします。

メインメニュークラッシュ後
README の画像README の画像

ℹ️ 本番環境で画像を読み込む場合は、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 ファイルをご覧ください。


🤝 貢献

プルリクエストを歓迎します。プルリクエストを作成する前に、貢献ガイドと行動規範をご確認ください。

  1. リポジトリをフォークします
  2. フィーチャーブランチを作成します:git checkout -b feature/my-feature
  3. 変更をコミットします:git commit -m 'Add: my feature'
  4. ブランチをプッシュします:git push origin feature/my-feature
  5. プルリクエストを作成します

バグ報告や機能リクエストは、イシューを作成してください。


📄 ライセンス

MIT。ライセンスの全文は LICENSE.mdをご覧ください。

Copyright © 2023 Craft & Graft LLC · GRARCHT ™ 2021


GRARCHT が心を込めて開発 · GRARCHT · Android Weekly #594 · Android Weekly #455

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。