sk2andy
sk2andy

Reputation: 819

Issue: Passing large data to second Activity

I've got an strange issue. I was looking around the web but didn't find an answer. I'm still a beginner in android programming. So let's go:

All I want to do is calling the second Activity with some data. It works fine with small data, but if the data gets large, the second Activity will not show and the first one finishes. Here's my code of the calling Method:

Intent intent = new Intent(ActivitySearch.this,ActivityResults.class);
Bundle bundle = new Bundle();
bundle.putParcelableArrayList("data", searchList);
intent.putExtras(bundle);
startActivity(intent);

The part of receiving data is not important. Even if I don't try to read the bundle, the activity will not be called. I've tested this with following lines:

@Override
public void onCreate(Bundle savedInstanceState) {
Log.d("DEBUG","ActivityResult::onCreate()");
super.onCreate(savedInstanceState);

OnCreate() gets never called.

Maybe one of yours got an idea... Thank you for your help!

Edit:at least I forgot: This only happens under ICS. The app works like a charme with gingerbread and froyo.

Edit2: Logcat

10-10 14:49:46.951: D/OpenGLRenderer(21696): Flushing caches (mode 0)
10-10 14:49:47.011: V/ActivityThread(22429): com.example.amazonsearch white listed for hwui
10-10 14:49:50.821: W/IInputConnectionWrapper(21696): showStatusIcon on inactive InputConnection

Upvotes: 48

Views: 41136

Answers (7)

Akash
Akash

Reputation: 206

If you pass a large amount of information from one Activity to another, then it may make the app slower.

Alternatively, use a global class to store variables, which you can use to easily get or set any values. This can be declared in a global file.

See this link: http://androidresearch.wordpress.com/2012/03/22/defining-global-variables-in-android/

Upvotes: 3

Eric
Eric

Reputation: 17566

I opted to write the payload to a file, then reading it back in the child activity because:

  • can survive the app being killed by the OS
    • (e.g. after going to the new activity, app is put into the background for a long time)
    • (e.g. Developer Options > Don't keep activities is turned on)
  • no need for global/static variables

this class below (DataReference) allow saves and loads a Serializable "Payload" to and from a file. the DataReference itself is Serializable so that it can be written to a Bundle or Intent to be passed to a new Activity...

just make sure to also save this during saveInstanceState, and re-load it during onCreate...

/**
 * provides a means to pass around a large resource via a bundle. instead of storing the [Payload] in
 * the bundle, only a [sessionId], and [directoryName] is stored in a bundle, that can be used to
 * retrieve the actual [Payload] at a later time.
 */
class DataReference<Payload : Serializable>(
        private val directoryName: String,
        private val sessionId: String = randomAlphaNumericString()
) : Serializable {

    private fun directory(context: Context) = context.applicationContext.getDir(directoryName, Context.MODE_PRIVATE)
    private fun sessionIdFile(context: Context) = File(directory(context), "sessionId")
    private fun payloadFile(context: Context) = File(directory(context), "payload")

    /**
     * if the [sessionId] is valid, reads the [Payload] from persistent memory, and returns it to
     * the caller, or null, if no existing [Payload] exists; otherwise, if [sessionId] is invalid,
     * the [Payload] is deleted from persistent memory (if any exists), and null is returned to the
     * caller.
     */
    fun load(context: Context, payloadKClass: KClass<Payload>): Payload? {
        return if (sessionId == sessionIdFile(context).readObject(String::class)) {
            payloadFile(context).readObject(payloadKClass)
        } else {
            sessionIdFile(context).deleteFileIfExists()
            payloadFile(context).deleteFileIfExists()
            null
        }
    }

    /**
     * overwrites any existing [Payload] with the new [payload], which is only accessible from
     * [load] when the [sessionId] passed into [load] matches the [sessionId] passed into this call
     * to [save].
     */
    fun save(context: Context, payload: Payload) {

        // delete files if they exist
        sessionIdFile(context).deleteFileIfExists()
        payloadFile(context).deleteFileIfExists()

        // write the payload & session id to the file
        ObjectOutputStream(sessionIdFile(context).outputStream()).use { oos ->
            oos.writeObject(sessionId)
            oos.flush()
        }
        ObjectOutputStream(payloadFile(context).outputStream()).use { oos ->
            oos.writeObject(payload)
            oos.flush()
        }
    }

    private fun File.deleteFileIfExists(): Boolean {
        isFile && delete()
        return !exists()
    }

    private fun <T:Any> File.readObject(tKClass: KClass<T>): T? = if (isFile) {
        ObjectInputStream(inputStream()).use { ois ->
            tKClass.safeCast(ois.readObject())
        }
    } else {
        null
    }
}

Upvotes: 1

Rishabh Sagar
Rishabh Sagar

Reputation: 4117

I faced this TransactionTooLargeException issue recently and was looking for a possible solution to avoid it. At last, I couldn't find anything which would work. In most of the answers, you will find people recommending different ways to avoid this exception, but a proper example wasn't available.

Here's what I have done to fix this issue, this might not be an ideal solution at a few places and has some limitation as well.

Step 1 - Write a class which will convert the bundle into a string and store it in Shared Preference.

public class ActivityBridge {

    private static final String KEY_ACTIVITY_BRIDGE = "ACTIVITY_BRIDGE";
    private final Context context;
    private SharedPreferences sharedPreferences;


    public ActivityBridge(Context context) {
        this.context = context;

        sharedPreferences = context.getSharedPreferences(KEY_ACTIVITY_BRIDGE, Context.MODE_PRIVATE);
    }


    @SuppressLint("ApplySharedPref")
    public void putData(Bundle bundle, Intent intent) {
        sharedPreferences.edit()
                .putString(
                        intent.toString(),
                        Base64.encodeToString(bundleToBytes(bundle), 0)
                )
                .commit();
    }


    @SuppressLint("ApplySharedPref")
    public Bundle getData(Intent intent) {
        Bundle bundle;
        final String bundleString = sharedPreferences.getString(intent.toString(), "");

        if (TextUtils.isEmpty(bundleString)) {
            return null;
        } else {
            bundle = bytesToBundle(Base64.decode(bundleString, 0));
        }

        sharedPreferences.edit()
                .clear()
                .commit();

        return bundle;
    }


    public byte[] bundleToBytes(Bundle bundle) {
        Parcel parcel = Parcel.obtain();
        parcel.writeBundle(bundle);
        byte[] bytes = parcel.marshall();
        parcel.recycle();
        return bytes;
    }


    public Bundle bytesToBundle(byte[] bytes) {
        Parcel parcel = Parcel.obtain();
        parcel.unmarshall(bytes, 0, bytes.length);
        parcel.setDataPosition(0);
        Bundle bundle = parcel.readBundle(context.getClassLoader());
        parcel.recycle();
        return bundle;
    }
}

Step 2 - Usage

While creating Intent

         Intent intent = new Intent(ActivityA.this, ActivityB.class);

         Bundle bundle = new Bundle();
         bundle.putString("<KEY>", "<VALUE>");
         new ActivityBridge(ActivityA.this).putData(bundle, intent);

         startActivity(intent);

while extracting Bundle

    Bundle bundle = new ActivityBridge(this).getData(getIntent());

Note: This solution clears the stored Bundle after being read and will not return the Bundle if the Activity is recreated. This is a workaround and any suggestion or issues will be highly appreciated.

Upvotes: 3

Rahim
Rahim

Reputation: 740

The way I prefer passing large data is through using enums. Some advantages of this approach:

  • No need to create singletons, you always have single instance of your enum;
  • Data is properly encapsulated;
  • Reference is deleted right after the activity receives it

Here is an example:

package com.jyvee.arguments;

import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

public class SomeActivity extends Activity {

    // Names for the arguments we pass to the
    // activity when we create it
    private final static String ARG_STRING = "ARG_STRING";
    private final static String ARG_INT = "ARG_INT";

    private String stringField;
    private int intField;
    private List<Object> arrayField;

    private enum DataHolder {
        INSTANCE;

        private List<Object> mObjectList;

        public static boolean hasData() {
            return INSTANCE.mObjectList != null;
        }

        public static void setData(final List<Object> objectList) {
            INSTANCE.mObjectList = objectList;
        }

        public static List<Object> getData() {
            final List<Object> retList = INSTANCE.mObjectList;
            INSTANCE.mObjectList = null;
            return retList;
        }
    }

    @Override
    protected void onCreate(final Bundle savedState) {
        super.onCreate(savedState);

        // Get the activity intent if there is a one
        final Intent intent = getIntent();

        // And retrieve arguments if there are any
        if (intent.hasExtra(ARG_STRING)) {
            stringField = intent.getExtras().getString(ARG_STRING);
        }
        if (intent.hasExtra(ARG_INT)) {
            intField = intent.getExtras().getInt(ARG_INT);
        }
        // And we retrieve large data from enum
        if (DataHolder.hasData()) {
            arrayField = DataHolder.getData();
        }

        // Now stringField, intField fields are available
        // within the class and can be accessed directly
    }

    /**
     * /** A static method for starting activity with supplied arguments
     * 
     * @param contextA
     *            context that starts this activity
     * @param stringArg
     *            A string argument to pass to the new activity
     * @param intArg
     *            An int argument to pass to the new activity
     * @param objectList
     *            An object list argument to pass to the new activity
     */
    public static void startActivity(final Context context, final String stringArg, 
            final int intArg, final List<Object> objectList) {

        // Initialize a new intent
        final Intent intent = new Intent(context, SomeActivity.class);

        // To speed things up :)
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);

        // And add arguments to the Intent
        intent.putExtra(ARG_STRING, stringArg);
        intent.putExtra(ARG_INT, intArg);

        // Now we put the large data into our enum instead of using Intent extras
        DataHolder.setData(objectList);

        context.startActivity(intent);
    }
}

More info here.

Upvotes: 37

Igor B.
Igor B.

Reputation: 1134

You are probably getting TransactionTooLargeException

As suggested by google android guide, you could use static fields or singletons to share data between activities.

They recommend it "For sharing complex non-persistent user-defined objects for short duration"

From your code it seems that's exactly what you need.

So your code in ActivitySearch.class could look something like this:

ActivityResults.data = searchList;
Intent intent = new Intent(ActivitySearch.this,ActivityResults.class);
startActivity(intent);

Then you can access ActivityResults.data from anywhere in ActivityResults activity after it starts.

For data that need to be shared between user sessions, it's not advisable to use static fields, since application process could be killed and restarted by android framework while app is running in background (if framework need to free resources). In such case all static fields will be reinitialized.

Upvotes: 59

Leandroid
Leandroid

Reputation: 2047

I have no idea why it doesn't work with large data, but if you don't find any way to fix it, I suggest you to use a custom global application, like here. (Also check the correct answer to make it works)

Upvotes: 1

waqaslam
waqaslam

Reputation: 68187

As far as i remembered, up till API-8 (Froyo), there were some limitations (like 1MB) when passing parcelable objects through intents. However, you may simply write down your parcelable data into a file and send the file path to your next activity through bundle. Later, code your second activity to read the data from file and delete it afterwards.

Upvotes: 3

Related Questions