/ android

Avoiding TransactionTooLargeException on Android Nougat and up

Judging by StackOverFlow, one of the most breaking updates for Android Nougat (7) has been the new way Android now handles saved instance states.

Everytime your app is either being sent to the background or being shut down, Android saves a lean representation of its current state called an Instance State. This has been the case since Android 1.

On Android Nougat, Google has started enforcing a 1mb size limit on this state. Anything over that limit is causing your app to crash with the exception being TransactionTooLargeException, when your targetSDK is >=24. On 23 your app won't crash, but you'd still get warning.

The cure

There has been many solutions suggested for this problem. There is even a library for saving the instance state to the file system instead of providing it to the operation system, called IcePick.

While most of these solutions require extensive changes to the app's code, most developers just want their state saved the way it used to be. Yes - you should always investigate the root cause of your saved insatnce state being bloated, but sometimes you need a quick solution that would allow you to upgrade your app to Naougat quickly.

TL;DR

We can borrow a solution purposed by Google meant for handling orientation changes, and save the state into a retainable Fragment instead of to the OS itself.

The purposed solution also frees memory when the saved state is consumed, and handles complex state changes.

First we need to create a view-less Fragment that would retain the state:

package info.peakapps.peaksdk.logic;

import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;

/**
 * A neat trick to avoid TransactionTooLargeException while saving our instance state
 */

public class SavedInstanceFragment extends Fragment {

    private static final String TAG = "SavedInstanceFragment";
    private Bundle mInstanceBundle = null;

    public SavedInstanceFragment() { // This will only be called once be cause of setRetainInstance()
        super();
        setRetainInstance( true );
    }

    public SavedInstanceFragment pushData( Bundle instanceState )
    {
        if ( this.mInstanceBundle == null ) {
            this.mInstanceBundle = instanceState;
        }
        else
        {
            this.mInstanceBundle.putAll( instanceState );
        }
        return this;
    }

    public Bundle popData()
    {
        Bundle out = this.mInstanceBundle;
        this.mInstanceBundle = null;
        return out;
    }

    public static final SavedInstanceFragment getInstance(FragmentManager fragmentManager )
    {
        SavedInstanceFragment out = (SavedInstanceFragment) fragmentManager.findFragmentByTag( TAG );

        if ( out == null )
        {
            out = new SavedInstanceFragment();
            fragmentManager.beginTransaction().add( out, TAG ).commit();
        }
        return out;
    }
}

Next thing is to save the instance state to the SavedInstanceFragment instead of the OS. We do this by patching the save instance mechanism in our Activity:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        SavedInstanceFragment.getInstance( getFragmentManager() ).pushData( (Bundle) outState.clone() );
        outState.clear(); // We don't want a TransactionTooLargeException, so we handle things via the SavedInstanceFragment
    }

And lastly we pop the saved instance when needed:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(SavedInstanceFragment.getInstance(getFragmentManager()).popData());
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState( SavedInstanceFragment.getInstance( getFragmentManager() ).popData() );
    }

No need to add this patch the the Fragments themselves since their state is being saved to their parent Activity.