Shuttle
Stop patching TransactionTooLargeException. Shuttle eliminates it at the source by warehousing large Serializable objects and passing only a lightweight identifier. No crashes. No code review babysitting.
Install / Use
/learn @grarcht/ShuttleREADME
<a href="https://github.com/grarcht/Shuttle/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/grarcht/shuttle?color=white&style=plastic" alt="License: MIT"/></a> <a href="https://search.maven.org/artifact/com.grarcht.shuttle/framework"><img src="https://img.shields.io/maven-central/v/com.grarcht.shuttle/framework?color=teal&style=plastic" alt="Maven Central"/></a> <a href="https://developer.android.com/studio/releases/platforms"><img src="https://img.shields.io/badge/Min%20API-21-brightgreen?style=plastic" alt="Min API"/></a> <a href="https://kotlinlang.org/"><img src="https://img.shields.io/badge/Kotlin-2.0%2B-purple?style=plastic" alt="Kotlin"/></a>
<br/><a href="https://androidweekly.net/issues/issue-594"><img src="https://img.shields.io/badge/Android%20Weekly-Issue%20%23594-orange?style=flat" alt="Android Weekly #594"/></a> <a href="https://androidweekly.net/issues/issue-455"><img src="https://img.shields.io/badge/Android%20Weekly-Issue%20%23455-orange?style=flat" alt="Android Weekly #455"/></a>
</div>Get To It Quick
- Why Shuttle?
- How It Works
- Quick Start
- Usage
- Architecture
- Demo Apps
- Heads Up: Know the Tradeoffs
- Contributing
- License
🚨 Why Shuttle?
🗞️ Featured in Android Weekly #594 and Android Weekly #455, validated by the Android community twice.
You've seen this before. Maybe last week:
android.os.TransactionTooLargeException: data parcel size X bytes
It didn't show up in dev. It didn't show up in QA. It showed up at 2am, in production, for real users. Your Play Store rating took the hit before anyone on the team even knew.
So you triaged it. Filed the ticket. Wrote the fix. Reviewed the PR. Ran QA again. Cut the hotfix. And then you added it to the code review checklist, hoping the next engineer would catch it before it happened again.
They won't. Not reliably. You can't review your way out of a structural problem.
Shuttle provides a modern, guarded way to pass large Serializable objects with Intent objects or save them in Bundle objects to avoid app crashes. The crash class is structurally prevented, not governed against.
Why keep spending more time and money on governance through code reviews? Why not embrace the problem by providing a solution for it?
Shuttle reduces the high level of governance needed to catch TransactionTooLargeException inducing code by:
- storing the
Serializableand passing an identifier for theSerializable - using a small-sized
Bundlefor binder transactions - avoiding app crashes from
TransactionTooLargeExceptions - enabling retrieval of the stored
Serializableat the destination
Shuttle also excels by:
- providing a solution with maven artifacts
- providing Solution Building Blocks (SBBs) for building on
- saving time by avoiding DB and table setup, especially when creating many tables for the content of different types of objects
When envisioning, designing, and creating the architecture, quality attributes and best practices were kept in mind. These attributes include usability, readability, recognizability, reusability, maintainability, and more.
| Without Shuttle | With Shuttle |
|---|---|
| Large Serializable passed in Intent/Bundle | Object stored in a warehouse; only a small identifier is passed |
| Silent in dev, catastrophic in production | Binder transaction stays within safe size limits, everywhere |
| Time and money spent on crash investigation, fixes, QA, and hotfixes | Crash class is structurally impossible |
| Requires constant code review governance | Ship with confidence |
| Engineers manually manage object lifecycles | Automatic or on-demand cargo cleanup built in |
| <img src="/media/videos/app_crash.gif" width="100%"/> | <img src="/media/videos/loaded_image_cargo.gif" width="100%"/> |
⚙️ How It Works
The Shuttle framework takes its name from cargo transportation in the freight industry. Moving and storage companies experience scenarios where large moving trucks cannot transport cargo the entire way to the destination (warehouses, houses, et cetera). These scenarios might occur from road restrictions, trucks being overweight from large cargo, and more. As a result, companies use small Shuttle vans to transport smaller cargo groups on multiple trips to deliver the entire shipment.
After the delivery is complete, employees remove the cargo remnants from the shuttle vans and trucks. This clean-up task is one of the last steps for the job.
The Shuttle framework takes its roots in these scenarios:
- creating a smaller cargo bundle object to use in successfully delivering the data to the destination
- shuttling the corresponding large cargo to a warehouse and storing it for pickup
- linking the smaller cargo with the larger cargo by an identifier
- providing a single source of truth (Shuttle interface) to use for transporting cargo
- providing convenience functions to remove cargo (automatically or on-demand)
Shuttle applies this same logic to Android's binder transaction limit:
┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘
🚀 Quick Start
1. Add Dependencies
Kotlin DSL (build.gradle.kts):
implementation("com.grarcht.shuttle:framework:3.0.3")
implementation("com.grarcht.shuttle:framework-integrations-persistence:3.0.3")
implementation("com.grarcht.shuttle:framework-integrations-extensions-room:3.0.3")
implementation("com.grarcht.shuttle:framework-addons-navigation-component:3.0.3") // Optional
Version Catalog (libs.versions.toml):
[versions]
shuttle = "3.0.3"
[libraries]
shuttle-framework = { group = "com.grarcht.shuttle", name = "framework", version.ref = "shuttle" }
shuttle-persistence = { group = "com.grarcht.shuttle", name = "framework-integrations-persistence", version.ref = "shuttle" }
shuttle-room = { group = "com.grarcht.shuttle", name = "framework-integrations-extensions-room", version.ref = "shuttle" }
shuttle-navigation = { group = "com.grarcht.shuttle", name = "framework-addons-navigation-component", version.ref = "shuttle" }
2. Ship Your First Cargo
// 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()
}
}
}
That's it. No custom DB setup. No table management. No crash.
📦 Usage
The recommended entry point is the Shuttle interface with CargoShuttle as the implementation. It's a single source of truth for all cargo transport operations.
Transport with Intents
Source component:
val cargoId = ImageMessageType.ImageData.value
shuttle.intentCargoWith(context, MVCSecondControllerActivity::class.java)
.transport(cargoId, imageModel)
.cleanShuttleOnReturnTo(
MVCFirstControllerFragment::class.java,
MVCSecondControllerActivity::class.java,
cargoId
)
.deliver(context)
ℹ️
cleanShuttleOnReturnTois important. It ensures cargo is purged from the Warehouse when it's no longer needed.
Transport with the Navigation Component
Source fragment:
val cargoId = ImageMessageType.ImageData.value
navController.navigateWithShuttle(shuttle, R.id.MVVMNavSecondViewActivity)
?.logTag(LOG_TAG)
?.transport(cargoId, imageModel as Serializable)
?.cleanShuttleOnReturnTo(
MVVMNavFirstViewFragment::class.java,
MVV
