Fragment bundle size with Hilt and ViewModel

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.

ViewModel & SavedStateHandle

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.

Hilt - compounding the SavedStateHandle problem

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 ViewModels, you end up with the same Fragment arguments Bundle saved four times: once in the Fragment and once for each ViewModel.

A workaround

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!

Wrap-up

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!

Semantics in Jetpack Compose Slides

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 with Jetpack Compose

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!

Role Semantics in Jetpack Compose

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.

Semantics and TalkBack with Jetpack Compose

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.