mcpField
S
Shuttle

Shuttle

本地服务
github.com/grarcht开发工具8713 周前更新

关于 Shuttle

这是一个 Android 框架,用于在组件之间安全传递大型 Serializable 对象,通过将数据存入仓库并只传递标识符,避免 TransactionTooLargeException 崩溃。适合面向生产环境的 Android 应用开发者。

主要功能

  1. 01将大对象存储并通过标识符传递
  2. 02基于 Intent 与 Navigation Component 的传输
  3. 03LCE 模式的密封类结果
  4. 04@ShuttleCargo 注解与 KSP 代码生成
  5. 05支持 Room 默认持久化实现

适用场景

  1. 01在 Activity 或 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 崩溃。

一款现代 Android 框架,可在组件间安全传输大型 Serializable 对象,避免崩溃。


许可证:MIT Maven Central API 文档 最低 SDK Kotlin


Android Weekly 第 594 期 Android Weekly 第 455 期


快速上手


🚨 为什么选择 Shuttle?

🗞️ 曾登上 Android Weekly #594 和 Android Weekly #455,两度获得 Android 社区认可。

你以前肯定遇到过这种情况。也许就在上周:

android.os.TransactionTooLargeException: data parcel size X bytes

它在开发环境中没有出现,在 QA 中也没有出现。凌晨 2 点,它在生产环境中出现了,影响了真实用户。团队里还没人知道发生了什么,你的 Play Store 评分就已经受到了影响。

于是你开始排查问题、提交工单、编写修复代码、审查 PR、重新进行 QA、发布热修复。然后,你把这个问题加进代码审查清单,希望下一位工程师能在问题再次发生前发现它。

但他们做不到,也无法始终做到。结构性问题无法靠审查来解决。

Shuttle 提供了一种现代且安全的方式,用 Intent 对象传递大型 Serializable 对象,或将其保存在 Bundle 对象中,从而避免应用崩溃。这类崩溃从结构上被杜绝,而不是靠监管来防范。

为什么还要继续投入更多时间和金钱,通过代码审查来进行监管?何不直面问题,为其提供解决方案?

Shuttle 通过以下方式,减少了发现会导致 TransactionTooLargeException 的代码所需的高强度监管:

  1. 存储 Serializable,并传递其标识符
  2. 使用小型 Bundle 进行 Binder 事务
  3. 避免应用因 TransactionTooLargeException 而崩溃
  4. 支持在目标位置取回已存储的 Serializable

Shuttle 还有以下优势:

  1. 通过 Maven 构件提供解决方案
  2. 提供可用于构建的解决方案构建模块(SBB)
  3. 省去数据库和表的设置时间,尤其是在需要为不同类型的对象内容创建大量表时

在构思、设计和构建架构时,我们始终将质量属性和最佳实践牢记于心。这些属性包括易用性、可读性、易识别性、可复用性、可维护性等。

不使用 Shuttle使用 Shuttle
在 Intent/Bundle 中传递大型 Serializable将对象存储在仓库中;只传递一个小型标识符
在开发环境中悄无声息,在生产环境中造成灾难性后果Binder 事务始终保持在安全大小限制内
花费时间和金钱调查崩溃、修复问题、进行 QA 和发布热修复从结构上杜绝此类崩溃
需要持续通过代码审查进行监管放心发布
工程师手动管理对象生命周期内置自动或按需清理货物
README 配图README 配图

⚙️ 工作原理

让 Android 应用免于崩溃 | Shuttle 框架与解决方案

Shuttle 框架的名称源自货运行业的货物运输。搬运和仓储公司会遇到这样的情况:大型搬家卡车无法将货物一路运送到目的地(仓库、房屋等)。这种情况可能是由道路限制、卡车因装载大量货物而超载等原因造成的。因此,公司会使用小型 Shuttle 厢式货车,分多次运输较小批次的货物,以运送整批货物。

交付完成后,员工会从 Shuttle 厢式货车和卡车中清除剩余货物。这项清理工作是任务的最后步骤之一。

Shuttle 框架源自以下场景:

  • 创建较小的货物包对象,以便成功将数据送达目的地
  • 将对应的大件货物运送到仓库并存放,等待领取
  • 通过标识符将较小货物与较大货物关联起来
  • 提供用于运输货物的单一事实来源(Shuttle 接口)
  • 提供移除货物的便捷函数(自动移除或按需移除)

Shuttle 将相同的逻辑应用于 Android 的 Binder 事务大小限制:

┌─────────────────────────────────────────────────────────────┐
│  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 最低 SDK26
Kotlin2.2.10
AGP(Android Gradle Plugin)8.0+
KSP仅在使用 @ShuttleCargo 时需要
Java21

🚀 快速开始

1. 添加依赖项

settings.gradle.kts — 应用 Shuttle Gradle 插件,将 @ShuttleCargo 注解处理器接入构建流程:

@ShuttleCargo 用于标注你想要在屏幕之间传输的货物类。在构建时,注解处理器会生成 Shuttle 存储和获取货物所需的序列化代码,无需手动编写 Serializable 样板代码。

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()
            }
        }
}

就这么简单。无需自定义数据库设置,无需管理表,也不会崩溃。


📦 用法

推荐的入口是 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 传输

源 Fragment:

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 会返回密封类结果,以支持 加载-内容-错误(LCE) 模式,让使用者全面掌控 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 是一个分层的 Solution Building Block(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-plugin自动配置 KSP 和编译器插件的 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 的因素之一。

涵盖两种架构模式:

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 提供了所需的处理入口,让 UI 能够妥善应对稍长的加载时间。

这些都是有据可查的权衡,而不是缺陷。架构设计始终需要权衡利弊,而这项选择值得。


🤖 AI

Model Context Protocol(MCP)

Model Context Protocol 是一项开放标准,可让 AI 助手连接外部工具和数据源。借助 MCP,助手不再只能依赖训练数据,而是能在你需要时调用实时工具,生成准确的样板代码、分析实际代码,并返回最新文档,全程无需离开编辑器。

Shuttle 已将 MCP 服务发布到 MCP Registry,这意味着任何开发者都可以让兼容 MCP 的编辑器连接该服务,在自己的项目中获取 Shuttle 专业知识,无需克隆或打开此代码仓库。你的 AI 助手可以为 Hilt、Koin 或手动接线搭建完整的 Shuttle 集成,并提供可直接粘贴的 Kotlin 样板代码;可以检测你粘贴的任何 Kotlin 或 Java 代码片段中是否存在 TransactionTooLargeException 风险;还可以按需检索权威的 Shuttle 规范,涵盖核心概念、设置、传输、获取、清理和注解。

对于团队而言,这是仅靠代码审查无法实现的治理效能倍增。无论经验水平或编辑器偏好如何,每位开发者从第一天起都能获得关于集成模式和风险的相同、准确且最新的指导。

请参阅 MCP README 文件,了解支持的编辑器完整列表和验证清单。


🤝 贡献

欢迎提交拉取请求。提交前,请阅读贡献指南和行为准则。

  1. Fork 此代码仓库
  2. 创建功能分支:git checkout -b feature/my-feature
  3. 提交更改:git commit -m 'Add: my feature'
  4. 推送分支:git push origin feature/my-feature
  5. 提交拉取请求

如需报告 Bug 或提出功能请求,请提交 Issue。


📄 许可证

MIT。完整条款请参阅 LICENSE.md。

版权所有 © 2023 Craft & Graft LLC · GRARCHT ™ 2021


评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。