关于 Shuttle
这是一个 Android 框架,用于在组件之间安全传递大型 Serializable 对象,通过将数据存入仓库并只传递标识符,避免 TransactionTooLargeException 崩溃。适合面向生产环境的 Android 应用开发者。
主要功能
- 01将大对象存储并通过标识符传递
- 02基于 Intent 与 Navigation Component 的传输
- 03LCE 模式的密封类结果
- 04@ShuttleCargo 注解与 KSP 代码生成
- 05支持 Room 默认持久化实现
适用场景
- 01在 Activity 或 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工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
快速上手
🚨 为什么选择 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 的代码所需的高强度监管:
- 存储
Serializable,并传递其标识符 - 使用小型
Bundle进行 Binder 事务 - 避免应用因
TransactionTooLargeException而崩溃 - 支持在目标位置取回已存储的
Serializable
Shuttle 还有以下优势:
- 通过 Maven 构件提供解决方案
- 提供可用于构建的解决方案构建模块(SBB)
- 省去数据库和表的设置时间,尤其是在需要为不同类型的对象内容创建大量表时
在构思、设计和构建架构时,我们始终将质量属性和最佳实践牢记于心。这些属性包括易用性、可读性、易识别性、可复用性、可维护性等。
| 不使用 Shuttle | 使用 Shuttle |
|---|---|
在 Intent/Bundle 中传递大型 Serializable | 将对象存储在仓库中;只传递一个小型标识符 |
| 在开发环境中悄无声息,在生产环境中造成灾难性后果 | Binder 事务始终保持在安全大小限制内 |
| 花费时间和金钱调查崩溃、修复问题、进行 QA 和发布热修复 | 从结构上杜绝此类崩溃 |
| 需要持续通过代码审查进行监管 | 放心发布 |
| 工程师手动管理对象生命周期 | 内置自动或按需清理货物 |
![]() | ![]() |
⚙️ 工作原理
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 最低 SDK | 26 |
| Kotlin | 2.2.10 |
| AGP(Android Gradle Plugin) | 8.0+ |
| KSP | 仅在使用 @ShuttleCargo 时需要 |
| Java | 21 |
🚀 快速开始
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-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 的因素之一。
涵盖两种架构模式:
MVVM: Activity/Fragment 作为 View,ViewModel 负责持有状态并充当协调者,Kotlin Channels 用于异步通知。
流程 1:使用 Shuttle 导航 ✅
点击 “使用 Shuttle 导航” -> 通过仓库取货成功加载图像。
| 主菜单 | Cargo 已卸载 |
|---|---|
![]() | ![]() |
流程 2:正常导航 ❌
点击 “正常导航” -> 应用因 TransactionTooLargeException 崩溃。
| 主菜单 | 崩溃后 |
|---|---|
![]() | ![]() |
⚠️ 注意:了解权衡
同一个 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 文件,了解支持的编辑器完整列表和验证清单。
🤝 贡献
- Fork 此代码仓库
- 创建功能分支:
git checkout -b feature/my-feature - 提交更改:
git commit -m 'Add: my feature' - 推送分支:
git push origin feature/my-feature - 提交拉取请求
如需报告 Bug 或提出功能请求,请提交 Issue。
📄 许可证
MIT。完整条款请参阅 LICENSE.md。
版权所有 © 2023 Craft & Graft LLC · GRARCHT ™ 2021
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。




