TransactionTooLargeException can be a difficult crash to fix on Android. For developers who haven’t had the joy of troubleshooting TransactionTooLargeException
before, here’s an overview:
When your Activity is going into the background onSaveInstanceState()
is your application’s opportunity to persist any transient state to a Bundle
. This is useful for maintaining state that would otherwise be lost, such as text a user has entered
into a text field. Ultimately this results in a Binder
transaction - one of Android’s primary tools for inter-process communication (IPC). This allows Android to save your bundled state outside of your process so if your process is killed you can restore that state later.
Binder transactions come with a limitation. The transaction buffer (which is shared across your entire process) is capped at 1MB. While the documented cap is 1MB, it appears that in practice the OS will TransactionTooLargeException
when your application attempts to save more than 500KB.
Beacuse of this limitation, Google suggests saving no more than 50kb of data total. That includes saved view state, anything you explicitly save in onSaveInstanceState()
, and Fragment arguments.
ViewModels often contain application state that we may want to persist to our application’s saved instance state. AndroidX offers a “Saved State module for ViewModel” (lifecycle-viewmodel-savedstate
) that gives ViewModels an API for doing just that. Simply add a SavedStateHandle
argument to your ViewModel’s constructor, and the default ViewModel provider will take care of hooking up the state saving and restoring mechanisms.
When using a ViewModel
with a Fragment
, that Fragment’s arguments are included in the SavedStateHandle
by default, giving you easy access to fragment arguments in your ViewModel
.
There’s a catch here - your Fragment
is going to save its arguments already, and the SavedStateHandle
is going to save all of its contents separately. That means that we are going to end up with a duplicate copy of the Fragment arguments: one in the Fragment
’s saved state and one in the SavedStateHandle
’s state.
This generally isn’t a problem if you are only including very small amounts of data in your Fragment arguments such as a string or two. However if your Fragment is already putting too much data or large objects into the arguments this will multiply the impact of those arguments.
The issue with duplicating Fragment
arguments can exponentially increase if you use Hilt to inject multiple view models in a single screen.
The default SavedStateViewModelFactory
only creates a SavedStateHandle
if the ViewModel
requests one. This is exactly what we want - don’t bother creating a SavedStateHandle
if we don’t need one.
Hilt unfortunately has to support not just injecting your ViewModel
constructor directly, but also injecting a SavedStateHandle
into any of your ViewModel
’s dependencies. As a result, Hilt will create a SavedStateHandle
for every ViewModel
even if are not using it!
What that means is that if your Fragment
uses three different ViewModel
s, you end up with the same Fragment arguments Bundle
saved four times: once in the Fragment and once for each ViewModel
.
The good news is that you can mitigate this! Before I show you how, keep in mind that the best long-term “fix” here is to ensure your arguments are as small as possible. For sufficiently small arguments, this issue is very minor. But if you know your arguments are already too big this can give you a little bit of breathing room.
Fragment.getDefaultViewModelCreationExtras() is where Fragment passes its arguments to SavedStateHandle
(source here). We can override that method and choose to exclude our Fragment’s arguments.
override val defaultViewModelCreationExtras: CreationExtras
get() {
// Sadly MutableCreationExtras() doesn't support
// removing key-value pairs, so we need to selectively
// copy the pieces we want to keep
val extras = MutableCreationExtras()
// These default extras are taken from
// Fragment.getDefaultViewModelCreationExtras()
extras.copyExtra(
defaultExtras,
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
)
extras.copyExtra(defaultExtras, SAVED_STATE_REGISTRY_OWNER_KEY)
extras.copyExtra(defaultExtras, VIEW_MODEL_STORE_OWNER_KEY)
return extras
}
/**
* Copy a CreationExtra from one MutableCreationExtras to another
*/
private fun <T : Any> MutableCreationExtras.copyExtra(
other: CreationExtras,
key: CreationExtras.Key<T>
) {
other.get(key)?.let { value ->
set(key, value)
}
}
This results in nearly identical ViewModel
creation, just without the Fragment arguments.
Note - if you are actually using the Fragment arguments in your ViewModel
, you don’t want to do this!
This is definitely not something I recommend doing proactively. It can lead to unexpected behavior later down the line if someone wants to use those Fragment arguments in a ViewModel
. It is also a premature optimization if your application isn’t close to the binder transaction limit.
Before going down this path you should inspect your saved state bundles using something like TooLargeTool to see which bundles are problematic. Again, the best fix is really minimizing how much infomration you are putting in your Fragment arguments. Typically you want to keep arguments to simple identifiers, not entire objects.
In many cases that might be a longer-term project for you, and hopefully this trick with your SavedStateHandle
will help!
Check out the slides below from my talk on Semantics in Jetpack Compose for the Twin Cities Kotlin User Group, or watch the recording here
Autofill is one of my favorite features to come to Android in recent years. I never want to manually type in my address and credit card information again. Autofill makes filling out forms an absolute breeze!
With Jetpack Compose on the horizon I’ve been seeing a lot of questions around how to support autofill in Jetpack Compose. Yes, autofill is supported in Compose, and here’s how you use it!
Jetpack Compose alpha 9 introduced an accessibility change that I’m personally excited about. We can now specify a “role” semantic for our Composables that accessibility services can use to provide more context to users. This context can be important to help a visually impaired user understand how their actions will affect your application for interactable elements.
A great example is an element that allows the user to select from a list of options. Visually impaired users may not have an obvious way to tell whether that element behaves like a checkbox (multiple options are selectable) or a radio button (only one option is selectable at a time). The new role property is intended to convey that type of information.
One of our goals as Android developers should always be to make our our apps as usable as possible. That includes making our apps accessible for users with disabilities or other impairments that require them to use accessibility features such as screen readers to interact with our apps.
As I’ve started playing with Jetpack Compose I’ve been curious about how Compose handles providing information to accessibility services. In this article we are going to dive into how Jetpack Compose interacts with TalkBack. How do we provide content descriptions for images, or attach state labels to elements like checkboxes? We will answer those questions and more!
This is part two in my two-part series on Jetpack Compose’s semantics APIs. Part one of this series provides an introduction to the semantics framework as a whole.