Best way to store SparseBooleanArray in Bundle?

2019-02-03 07:40发布

问题:

When a config change happens, my ListView Checkbox states get lost, which I understand why. I try to implement

public void onSaveInstanceState(final Bundle outState)

in one of my Fragments. So I'm just wondering what's the easiest way to store my SparseBooleanArray in the outState.

Also, I'm a bit confused, as ListView has the method:

getListView().getCheckedItemPositions();

What's this good for?

回答1:

In my case, I ended up doing it by implementing a Parcelable wrapper around the SparseBooleanArray, like this:

import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseBooleanArray;

public class SparseBooleanArrayParcelable extends SparseBooleanArray implements Parcelable {
  public static Parcelable.Creator<SparseBooleanArrayParcelable> CREATOR = new Parcelable.Creator<SparseBooleanArrayParcelable>() {
    @Override
    public SparseBooleanArrayParcelable createFromParcel(Parcel source) {
      SparseBooleanArrayParcelable read = new SparseBooleanArrayParcelable();
      int size = source.readInt();

      int[] keys = new int[size];
      boolean[] values = new boolean[size];

      source.readIntArray(keys);
      source.readBooleanArray(values);

      for (int i = 0; i < size; i++) {
        read.put(keys[i], values[i]);
      }

      return read;
    }

    @Override
    public SparseBooleanArrayParcelable[] newArray(int size) {
      return new SparseBooleanArrayParcelable[size];
    }
  };

  public SparseBooleanArrayParcelable() {

  }

  public SparseBooleanArrayParcelable(SparseBooleanArray sparseBooleanArray) {
    for (int i = 0; i < sparseBooleanArray.size(); i++) {
      this.put(sparseBooleanArray.keyAt(i), sparseBooleanArray.valueAt(i));
    }
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    int[] keys = new int[size()];
    boolean[] values = new boolean[size()];

    for (int i = 0; i < size(); i++) {
      keys[i] = keyAt(i);
      values[i] = valueAt(i);
    }

    dest.writeInt(size());
    dest.writeIntArray(keys);
    dest.writeBooleanArray(values);
  }
}

This allows you to save and load your SparseBooleanArray by just doing:

Bundle bundle; //For your activity/fragment state, to include in an intent, ...
SparseBooleanArray sbarray; //Probably from your listview.getCheckedItemPositions()

//Write it
bundle.putParcelable("myBooleanArray", new SparseBooleanArrayParcelable(sbarray));

//Read it back
sbarray = (SparseBooleanArray) bundle.getParcelable("myBooleanArray");

Just my .02€



回答2:

I don't see any answers to the last part of the question:

Also, I'm a bit confused, as ListView has the method:

getListView().getCheckedItemPositions();

What's this good for?

Assuming you're using checkboxes in conjunction with Action Mode, the Action Mode itself will store the state of checked positions. In the normal mode, a press-and-hold will highlight an item, enable action mode, and then allow the user to tap other items to also check them.

What happens on an orientation change? Action mode is resumed and the highlights are kept. Your checkboxes aren't, but clearly the data of what is checked is.

After an orientation change, onCreateActionMode() is called. In this method (but not before), getListView().getCheckedItemPositions() will return the SparseBooleanArray you're probably looking for.

The nifty part about this is if this is your only use case for the SparseBooleanArray, you need not even worry about storing it yourself. You can retrieve it from the system which is already storing it across the orientation change.

That's what getListView().getCheckedItemPositions() is good for.



回答3:

You could either invent some serialization scheme for that data structure or switch to a HashSet and store only the list index positions in it which are checked. Since HashSet is serializable you can just put it into the instance state Bundle.



回答4:

Extend SparseArray to implement a Serializable:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import android.util.SparseArray;



/**
 * @author Asaf Pinhassi www.mobiledev.co.il
 * @param <E>
 *
 */
public class SerializableSparseArray<E> extends SparseArray<E> implements Serializable{

    private static final long serialVersionUID = 824056059663678000L;

    public SerializableSparseArray(int capacity){
        super(capacity);
    }

    public SerializableSparseArray(){
        super();
    }

    /**
     * This method is private but it is called using reflection by java
     * serialization mechanism. It overwrites the default object serialization.
     *
     * <br/><br/><b>IMPORTANT</b>
     * The access modifier for this method MUST be set to <b>private</b> otherwise {@link java.io.StreamCorruptedException}
     * will be thrown.
     *
     * @param oos
     *            the stream the data is stored into
     * @throws IOException
     *             an exception that might occur during data storing
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        Object[] data = new  Object[size()];

        for (int i=data.length-1;i>=0;i--){
            Object[] pair = {keyAt(i),valueAt(i)}; 
            data[i] = pair;
        }
        oos.writeObject(data);
    }

    /**
     * This method is private but it is called using reflection by java
     * serialization mechanism. It overwrites the default object serialization.
     *
     * <br/><br/><b>IMPORTANT</b>
     * The access modifier for this method MUST be set to <b>private</b> otherwise {@link java.io.StreamCorruptedException}
     * will be thrown.
     *
     * @param oos
     *            the stream the data is read from
     * @throws IOException
     *             an exception that might occur during data reading
     * @throws ClassNotFoundException
     *             this exception will be raised when a class is read that is
     *             not known to the current ClassLoader
     */
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        Object[] data = (Object[]) ois.readObject();
        for (int i=data.length-1;i>=0;i--){
            Object[] pair = (Object[]) data[i]; 
            this.append((Integer)pair[0],(E)pair[1]);
        }
        return;
    }


}


回答5:

I am trying to do the same thing. Initially i was using a HashMap

How to send hashmap value to another activity using an intent

to pass between activities since it extends the Serializable interface. However, after doing some research:

"SparseBooleanArrays are intended to be more efficient than using a HashMap to map Integers to Booleans."

However, I can't find a simple way to store this data structure. I have contemplated grabbing the values of the SparseBooleanArray and storing it in a HashMap so that it can be passed between activities. But this seems to add more complexity.

Unfortunately, I think I am going to revert to using HashMaps to store my list of checked boxes



回答6:

Here is my solution. Just a simple wrapper that can be used to serialise a SparseBooleanArray.

public class SerializableSparseBooleanArrayContainer implements Serializable {

    private static final long serialVersionUID = 393662066105575556L;
    private SparseBooleanArray mSparseArray;

    public SerializableSparseBooleanArrayContainer(SparseBooleanArray mDataArray) {
        this.mSparseArray = mDataArray;
    }

    public SparseBooleanArray getSparseArray() {
        return mSparseArray;
    }

    public void setSparseArray(SparseBooleanArray sparseArray) {
        this.mSparseArray = sparseArray;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.writeLong(serialVersionUID);
        int sparseArraySize = mSparseArray.size();
        out.write(sparseArraySize);
        for (int i = 0 ; i < sparseArraySize; i++){
            int key = mSparseArray.keyAt(i);
            out.writeInt(key);
            boolean value = mSparseArray.get(key);
            out.writeBoolean(value);
        }
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        long readSerialVersion = in.readLong();
        if (readSerialVersion != serialVersionUID) {
            throw new IOException("serial version mismatch");
        }
        int sparseArraySize = in.read();
        mSparseArray = new SparseBooleanArray(sparseArraySize);
        for (int i = 0 ; i < sparseArraySize; i++) {
            int key = in.readInt();
            boolean value = in.readBoolean();
            mSparseArray.put(key, value);
        }
    }

}

I then add the object to a bundle like so:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    SparseBooleanArray sparseBooleanArray = getSparseBooleanArray();
    SerializableSparseBooleanArrayContainer sparseBooleanArraySerializable = new SerializableSparseBooleanArrayContainer(sparseBooleanArray);
    outState.putSerializable(SOME_BUNDLE_KEY, sparseBooleanArraySerializable);
}


回答7:

I tried the solution creating SerializableSparseArray extending SparseArray making it possible to put a SparseArray into bundle via Bundle.putSerializable call.

But I found I can't obtain the saved object from bundle in onRestoreInstanceState. Digging into the issue I found that savedInstanceState.getSerializable(KEY_TO_STRING_SPARSE_ARRAY) == null.

Then, trying to inspect savedInstanceState.get(KEY_TO_STRING_SPARSE_ARRAY) and suprisingly got a SparseArray<String> not SerializableSparseArray<String>. Finally I'm using savedInstanceState.getSparseParcelableArray(KEY_TO_STRING_SPARSE_ARRAY) to obtaining SparseArray back from a bundle.

Then, using java reflection to save a SparseArray<String> to bundle directly without extending with Serializable or Parcelable interface. It's a little bit dirty but I think you can make your own utility function hiding the following detail implementation.

try {
    // FIXME WTF
    Method m = Bundle.class.getMethod("putSparseParcelableArray", String.class, SparseArray.class);
    m.invoke(savedInstanceState, KEY_TO_STRING_SPARSE_ARRAY, mSparseStringArray);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
    Log.e(TAG, "exception", e);
}

I've tested the code and it works on Android 4.4.4. And I'd like to know if it's safe to use the implementation in other SDK implementations.



回答8:

I would create a Parcelable holder class, using the native methods from Parcel in order to serialize/deserialize the SparseBooleanArray:

import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseBooleanArray;

public class SparseBooleanArrayHolder implements Parcelable {

    private final SparseBooleanArray source;

    public SparseBooleanArrayHolder(SparseBooleanArray source) {
        this.source = source;
    }

    public SparseBooleanArray get() {
        return source;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeSparseBooleanArray(source);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<SparseBooleanArrayHolder> CREATOR = new Creator<SparseBooleanArrayHolder>() {
        @Override
        public SparseBooleanArrayHolder createFromParcel(Parcel in) {
            return new SparseBooleanArrayHolder(in.readSparseBooleanArray());
        }

        @Override
        public SparseBooleanArrayHolder[] newArray(int size) {
            return new SparseBooleanArrayHolder[size];
        }
    };
}