Android custom/compound control: Parcelable state is shared and it shouldn't? - android

I have created a custom View / Compound Control in my Android application, which lets users "draw" their signature.
The signature is contained in a Canvas, linked to a Bitmap object.
I want the signature "image" to be preserved when the orientation is changed. Therefore, I have implemented onSaveInstanceState / onRestoreInstanceState by storing the Bitmap's pixels:
#Override
public Parcelable onSaveInstanceState()
{
// Call superclass method, retrieve Parcelable
Parcelable superState = super.onSaveInstanceState();
if (pictureBitmap_ != null)
{
// Retrieve the current pixels array.
int totalSize = pictureBitmap_.getWidth()
* pictureBitmap_.getHeight();
int pixels[] = new int[totalSize];
pictureBitmap_.getPixels(pixels,
0,
pictureBitmap_.getWidth(),
0,
0,
pictureBitmap_.getWidth(),
pictureBitmap_.getHeight());
// Create the saved state object.
SavedState savedState = new SavedState(superState);
savedState.pixels = pixels;
// Return the populated saved state object.
return savedState;
}
else
{
// Simply pass original Parcelable along
return superState;
}
}
#Override
public void onRestoreInstanceState(Parcelable state)
{
// Is the Parcelable an instance of our custom SavedState?
if (!(state instanceof SavedState))
{
// No, simply delegate to superclass.
super.onRestoreInstanceState(state);
return;
}
// Retrieve custom state object.
SavedState savedState = (SavedState) state;
// Use superclass to restore original state.
super.onRestoreInstanceState(savedState.getSuperState());
// Restore custom state (transfer pixels).
this.transferPixels_ = savedState.pixels;
// If the picture bitmap is already initialized...
if (pictureBitmap_ != null)
{
// Transfer the pixels right away.
transferBitmap();
// Refresh the canvas
drawSignature();
}
}
The integer array of pixels is stored in a SavedState object, which is a custom subclass of BaseSavedState:
private static class SavedState
extends BaseSavedState
{
// Members
int[] pixels;
// Methods
SavedState(Parcelable superState)
{
super(superState);
}
private SavedState(Parcel in)
{
super(in);
in.readIntArray(pixels);
}
#Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeIntArray(pixels);
}
// required field that makes Parcelables from a Parcel
#SuppressWarnings("unused")
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>()
{
public SavedState createFromParcel(Parcel in)
{
return new SavedState(in);
}
public SavedState[] newArray(int size)
{
return new SavedState[size];
}
};
}
Now, while this is working well if I only have ONE (1) instance of my custom view in my activity... a problem occurs if I have TWO (2) or more instances: when I flip the orientation, all my controls end up displaying the same signature, which was the last control's signature.
In other words, the last set of pixels stored in a SavedState object appears to become the "universal" set of pixels for all my control instances within my Activity.
How do I ensure that each control has its own independent saved state?

Related

How does android save sates of a View specifically a Preference?

in Settings documentation, there is an implementation of onSaveInstanceState and onRestoreInstanceState with a class called SaveSatet.
how does it work? can anyone explain to me?
private static class SavedState extends BaseSavedState {
// Member that holds the setting's value
// Change this data type to match the type saved by your Preference
int value;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
// Get the current preference's value
value = source.readInt(); // Change this to read the appropriate data type
}
#Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
// Write the preference's value
dest.writeInt(value); // Change this to write the appropriate data type
}
// Standard creator object using an instance of this class
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};}
and the implementation:
#Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
// Check whether this Preference is persistent (continually saved)
if (isPersistent()) {
// No need to save instance state since it's persistent,
// use superclass state
return superState;
}
// Create instance of custom BaseSavedState
final SavedState myState = new SavedState(superState);
// Set the state's value with the class member that holds current
// setting value
myState.value = mNewValue;
return myState;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
// Check whether we saved the state in onSaveInstanceState
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save the state, so call superclass
super.onRestoreInstanceState(state);
return;
}
// Cast state to custom BaseSavedState and pass to superclass
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
// Set this Preference's widget to reflect the restored state
mNumberPicker.setValue(myState.value);
}

Can PreferenceActivity customize how it handles touches?

I've been recently introduced to PreferenceActivity and would like to change how I handle interaction with an EditTextPreference defined in the xml.
I've put logs, toast and breakpoints over where boilerplate has overridden onListItemClick( but nothing is getting back to me. I've even tried stepping into the super class and was able to set breakpoints on the if and return successfully although they weren't ultimately trapping.
protected void onListItemClick(ListView l, View v, int position, long id) {
if (!isResumed()) {
return;
}
super.onListItemClick(l, v, position, id);
Thanks for looking
EDIT #DanielLe, here is my code:
//This isn't getting called?!
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
String selection = l.getItemAtPosition(position).toString();
Toast.makeText(this, selection, Toast.LENGTH_LONG).show();
Log.d("Activity", "onListItemClick=" + l.getItemAtPosition(position).toString());
super.onListItemClick(l, v, position, id);
}
At the risk of repeating what's gone before, one solution is to extend DialogPreferences as described in the Google guide for Android dev. It's only showing ok and cancel buttons which I believe makes it the minimum for a persisting DialogPreferences implementation:
Android: Creating custom preference
import android.content.Context;
import android.content.DialogInterface;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.DialogPreference;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
public class ClickablePreference extends DialogPreference {
private String mNewValue;
private EditText mEditText;
public ClickablePreference(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.dir_picker_dialog);
setPositiveButtonText(android.R.string.ok);
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
}
#Override
protected void onDialogClosed(boolean positiveResult) {
// When the user selects "OK", persist the new value
if (positiveResult) {
persistString(mNewValue);
}
}
private static class SavedState extends BaseSavedState {
// Member that holds the setting's value
// Change this data type to match the type saved by your Preference
String value;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
// Get the current preference's value
value = source.readString(); // Change this to read the appropriate data type
}
#Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
// Write the preference's value
dest.writeString(value); // Change this to write the appropriate data type
}
// Standard creator object using an instance of this class
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
#Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
if (restorePersistedValue) {
// Restore existing state
mNewValue = this.getPersistedString("");
} else {
// Set default state from the XML attribute
mNewValue = (String) defaultValue;
persistString(mNewValue);
}
}
#Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
// Check whether this Preference is persistent (continually saved)
if (isPersistent()) {
// No need to save instance state since it's persistent,
// use superclass state
return superState;
}
// Create instance of custom BaseSavedState
final SavedState myState = new SavedState(superState);
// Set the state's value with the class member that holds current
// setting value
myState.value = mNewValue;
return myState;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
// Check whether we saved the state in onSaveInstanceState
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save the state, so call superclass
super.onRestoreInstanceState(state);
return;
}
// Cast state to custom BaseSavedState and pass to superclass
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
// Set this Preference's widget to reflect the restored state
mEditText.setText(myState.value);
}
#Override
protected void onClick() {
super.onClick();
}
#Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
}
}
I've yet to look at Concise way of writing new DialogPreference classes? in detail although it seems very similar to what I got from Google.
The problem with this solution, apart from its size, is that mEditText is unused and I couldn't actually grab a reference to the displayed EditText defined in xml.
Roll on Solution 2
With thanks to Android: launch a custom Preference from a PreferenceActivity
just tagged this on the end of onPostCreate from MyPreferenceActivity
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
setupSimplePreferencesScreen();
Preference customPref = (Preference) findPreference("pref_do_something");
customPref.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Log.d("Activity", "onPreferenceClick=" + preference.toString());
return true;
}
});
}
Far better :)

Parcelable, what is newArray for?

I am implementing Parcelable in order to transmit some simple data throughout an Intent.
However, There is one method in the Parcelable interface that I don't understand at all : newArray().
It does not have any relevant documentation & is not even called in my code when I parcel/deparcel my object.
Sample Parcelable implementation :
public class MyParcelable implements Parcelable {
private int mData;
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}
public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
private MyParcelable(Parcel in) {
mData = in.readInt();
}
}
So, my question is : what is this method for ? and when is it called ?
Is there any point in doing something else than return new MyParcelable[size]; in that method ?
this is a function to be called when you try to deserialize an array of Parcelable objects and for each single object createFromParcel is called.
It is there to prepare the typed array without all the generics stuff. That's it.
Returning just the standard return new MyParcelable[size]; is fine.
It is normal, that you never call it yourself. However, by calling something like Bundle.getParcelableArray() you end up in this method indirectly.
newArray is responsible to create an array of our type of the appropriate size

Android manage configuration changes(state) in custom view

This is follow up to: Android local variable get's lost when using camera intent
Proper way to do it is to handle onSaveInstanceState and onRestoreInstanceState like shown here:
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/CompoundButton.java
Here is my code:
static class SavedState extends BaseSavedState
{
private String requestedFileName;
private UUID[] mImages = new UUID[4];
SavedState(Parcelable superState)
{
super(superState);
}
private SavedState(Parcel in)
{
super(in);
}
#Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
}
//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>()
{
public SavedState createFromParcel(Parcel in)
{
return new SavedState(in);
}
public SavedState[] newArray(int size)
{
return new SavedState[size];
}
};
}
#Override
public Parcelable onSaveInstanceState()
{
//begin boilerplate code that allows parent classes to save state
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
//end
ss.requestedFileName = this.requestedFileName;
ss.mImages = this.mImages;
return ss;
}
#Override
public void onRestoreInstanceState(Parcelable state)
{
//begin boilerplate code so parent classes can restore state
if(!(state instanceof SavedState))
{
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
//end
this.requestedFileName = ss.requestedFileName;
this.mImages = ss.mImages;
RestoreImageState();
}
Now to my question. This code seems to work properly and it handles all state changes without a problem. HOWEVER, if you look at SavedState.writeToParcel and SavedState.SavedState you will notice that I do not store my variables there. Do I have to? Why? Problem is that I understand how to wrteToParcel and my data types match. But reading out of parcel not so clear with complex types. And in my tests it wasn't called.
EDIT:
Does this look correct for save/retreive order?
private SavedState(Parcel in)
{
super(in);
this.mName = in.readString();
this.mIndex = in.readInt();
this.mApplicableDamages = (String[])in.readValue(null);
this.mSelectedDamages = (boolean[])in.readValue(null);
}
#Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeString(this.mName);
out.writeInt(this.mIndex);
out.writeArray(this.mApplicableDamages);
out.writeBooleanArray(this.mSelectedDamages);
}
You have to save these variables in onSaveInstanceState() and restore them in onRestoreInstanceState() later if they're part of your view's state and you don't want to lose them when a parent activity is recreated.
The variables may be lost because when the configuration change happens the parent activity is destroyed and a new activity object is recreated. This new object is receive the previously saved state. And if you don't add some variables to the state the won't be restored.
As to the writing complex types to and reading the from Parcel you can implement Parcelable for some parts of the complex type. Or you can just decompose the complex type to the primitive (parcelable) fields and store this fields one by one. In your case UUID[] can be stored in Parcel as Serializable or you can convert UUID[] to ParcelUuid[] and store this array as parcelable array.
And a few words about SavedState implementation. The fields in SavedState will not be saved if you don't add them to Parcel. So you have to write the to Parcel in SavedState.writeToParcel() method. And also you need to read them back in SavedState(Parcel) constructor.
static class SavedState extends BaseSavedState
{
private String requestedFileName;
private UUID[] mImages = new UUID[4];
SavedState(Parcelable superState)
{
super(superState);
}
private SavedState(Parcel in)
{
super(in);
requestedFileName = in.readString();
mImages = (UUID[])in.readSerializable();
}
#Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeString(requestedFileName);
out.writeSerializable(mImages);
}
//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>()
{
public SavedState createFromParcel(Parcel in)
{
return new SavedState(in);
}
public SavedState[] newArray(int size)
{
return new SavedState[size];
}
};
}

How to prevent custom views from losing state across screen orientation changes

I've successfully implemented onRetainNonConfigurationInstance() for my main Activity to save and restore certain critical components across screen orientation changes.
But it seems, my custom views are being re-created from scratch when the orientation changes. This makes sense, although in my case it's inconvenient because the custom view in question is an X/Y plot and the plotted points are stored in the custom view.
Is there a crafty way to implement something similar to onRetainNonConfigurationInstance() for a custom view, or do I need to just implement methods in the custom view which allow me to get and set its "state"?
I think this is a much simpler version. Bundle is a built-in type which implements Parcelable
public class CustomView extends View
{
private int stuff; // stuff
#Override
public Parcelable onSaveInstanceState()
{
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putInt("stuff", this.stuff); // ... save stuff
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state)
{
if (state instanceof Bundle) // implicit null check
{
Bundle bundle = (Bundle) state;
this.stuff = bundle.getInt("stuff"); // ... load stuff
state = bundle.getParcelable("superState");
}
super.onRestoreInstanceState(state);
}
}
You do this by implementing View#onSaveInstanceState and View#onRestoreInstanceState and extending the View.BaseSavedState class.
public class CustomView extends View {
private int stateToSave;
...
#Override
public Parcelable onSaveInstanceState() {
//begin boilerplate code that allows parent classes to save state
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
//end
ss.stateToSave = this.stateToSave;
return ss;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
//begin boilerplate code so parent classes can restore state
if(!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
//end
this.stateToSave = ss.stateToSave;
}
static class SavedState extends BaseSavedState {
int stateToSave;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}
#Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}
//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
The work is split between the View and the View's SavedState class. You should do all the work of reading and writing to and from the Parcel in the SavedState class. Then your View class can do the work of extracting the state members and doing the work necessary to get the class back to a valid state.
Notes: View#onSavedInstanceState and View#onRestoreInstanceState are called automatically for you if View#getId returns a value >= 0. This happens when you give it an id in xml or call setId manually. Otherwise you have to call View#onSaveInstanceState and write the Parcelable returned to the parcel you get in Activity#onSaveInstanceState to save the state and subsequently read it and pass it to View#onRestoreInstanceState from Activity#onRestoreInstanceState.
Another simple example of this is the CompoundButton
Easy with kotlin
#Parcelize
class MyState(val superSavedState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSavedState), Parcelable
class MyView : View {
var loading: Boolean = false
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return MyState(superState, loading)
}
override fun onRestoreInstanceState(state: Parcelable?) {
val myState = state as? MyState
super.onRestoreInstanceState(myState?.superSaveState ?: state)
loading = myState?.loading ?: false
//redraw
}
}
Here is another variant that uses a mix of the two above methods.
Combining the speed and correctness of Parcelable with the simplicity of a Bundle:
#Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// The vars you want to save - in this instance a string and a boolean
String someString = "something";
boolean someBoolean = true;
State state = new State(super.onSaveInstanceState(), someString, someBoolean);
bundle.putParcelable(State.STATE, state);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
State customViewState = (State) bundle.getParcelable(State.STATE);
// The vars you saved - do whatever you want with them
String someString = customViewState.getText();
boolean someBoolean = customViewState.isSomethingShowing());
super.onRestoreInstanceState(customViewState.getSuperState());
return;
}
// Stops a bug with the wrong state being passed to the super
super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE);
}
protected static class State extends BaseSavedState {
protected static final String STATE = "YourCustomView.STATE";
private final String someText;
private final boolean somethingShowing;
public State(Parcelable superState, String someText, boolean somethingShowing) {
super(superState);
this.someText = someText;
this.somethingShowing = somethingShowing;
}
public String getText(){
return this.someText;
}
public boolean isSomethingShowing(){
return this.somethingShowing;
}
}
The answers here already are great, but don't necessarily work for custom ViewGroups. To get all custom Views to retain their state, you must override onSaveInstanceState() and onRestoreInstanceState(Parcelable state) in each class.
You also need to ensure they all have unique ids, whether they're inflated from xml or added programmatically.
What I came up with was remarkably like Kobor42's answer, but the error remained because I was adding the Views to a custom ViewGroup programmatically and not assigning unique ids.
The link shared by mato will work, but it means none of the individual Views manage their own state - the entire state is saved in the ViewGroup methods.
The problem is that when multiple of these ViewGroups are added to a layout, the ids of their elements from the xml are no longer unique (if its defined in xml). At runtime, you can call the static method View.generateViewId() to get a unique id for a View. This is only available from API 17.
Here is my code from the ViewGroup (it is abstract, and mOriginalValue is a type variable):
public abstract class DetailRow<E> extends LinearLayout {
private static final String SUPER_INSTANCE_STATE = "saved_instance_state_parcelable";
private static final String STATE_VIEW_IDS = "state_view_ids";
private static final String STATE_ORIGINAL_VALUE = "state_original_value";
private E mOriginalValue;
private int[] mViewIds;
// ...
#Override
protected Parcelable onSaveInstanceState() {
// Create a bundle to put super parcelable in
Bundle bundle = new Bundle();
bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState());
// Use abstract method to put mOriginalValue in the bundle;
putValueInTheBundle(mOriginalValue, bundle, STATE_ORIGINAL_VALUE);
// Store mViewIds in the bundle - initialize if necessary.
if (mViewIds == null) {
// We need as many ids as child views
mViewIds = new int[getChildCount()];
for (int i = 0; i < mViewIds.length; i++) {
// generate a unique id for each view
mViewIds[i] = View.generateViewId();
// assign the id to the view at the same index
getChildAt(i).setId(mViewIds[i]);
}
}
bundle.putIntArray(STATE_VIEW_IDS, mViewIds);
// return the bundle
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
// We know state is a Bundle:
Bundle bundle = (Bundle) state;
// Get mViewIds out of the bundle
mViewIds = bundle.getIntArray(STATE_VIEW_IDS);
// For each id, assign to the view of same index
if (mViewIds != null) {
for (int i = 0; i < mViewIds.length; i++) {
getChildAt(i).setId(mViewIds[i]);
}
}
// Get mOriginalValue out of the bundle
mOriginalValue = getValueBackOutOfTheBundle(bundle, STATE_ORIGINAL_VALUE);
// get super parcelable back out of the bundle and pass it to
// super.onRestoreInstanceState(Parcelable)
state = bundle.getParcelable(SUPER_INSTANCE_STATE);
super.onRestoreInstanceState(state);
}
}
I had the problem that onRestoreInstanceState restored all my custom views with the state of the last view. I solved it by adding these two methods to my custom view:
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
#Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
To augment other answers - if you have multiple custom compound views with the same ID and they are all being restored with the state of the last view on a configuration change, all you need to do is tell the view to only dispatch save/restore events to itself by overriding a couple of methods.
class MyCompoundView : ViewGroup {
...
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}
For an explanation of what is happening and why this works, see this blog post. Basically your compound view's children's view IDs are shared by each compound view and state restoration gets confused. By only dispatching state for the compound view itself, we prevent their children from getting mixed messages from other compound views.
I found that this answer was causing some crashes on Android versions 9 and 10. I think it's a good approach but when I was looking at some Android code I found out it was missing a constructor. The answer is quite old so at the time there probably was no need for it. When I added the missing constructor and called it from the creator the crash was fixed.
So here is the edited code:
public class CustomView extends LinearLayout {
private int stateToSave;
...
#Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
// your custom state
ss.stateToSave = this.stateToSave;
return ss;
}
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
{
dispatchFreezeSelfOnly(container);
}
#Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// your custom state
this.stateToSave = ss.stateToSave;
}
#Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container)
{
dispatchThawSelfOnly(container);
}
static class SavedState extends BaseSavedState {
int stateToSave;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}
// This was the missing constructor
#RequiresApi(Build.VERSION_CODES.N)
SavedState(Parcel in, ClassLoader loader)
{
super(in, loader);
this.stateToSave = in.readInt();
}
#Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
// This was also missing
#Override
public SavedState createFromParcel(Parcel in, ClassLoader loader)
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new SavedState(in, loader) : new SavedState(in);
}
#Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
#Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
Instead of using onSaveInstanceState and onRestoreInstanceState, you can also use a ViewModel. Make your data model extend ViewModel, and then you can use ViewModelProviders to get the same instance of your model every time the Activity is recreated:
class MyData extends ViewModel {
// have all your properties with getters and setters here
}
public class MyActivity extends FragmentActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// the first time, ViewModelProvider will create a new MyData
// object. When the Activity is recreated (e.g. because the screen
// is rotated), ViewModelProvider will give you the initial MyData
// object back, without creating a new one, so all your property
// values are retained from the previous view.
myData = ViewModelProviders.of(this).get(MyData.class);
...
}
}
To use ViewModelProviders, add the following to dependencies in app/build.gradle:
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
Note that your MyActivity extends FragmentActivity instead of just extending Activity.
You can read more about ViewModels here:
Android Developer Guide, Handle configuration changes
Android Developer Guide, Saving UI States, Use ViewModel to handle configuration changes
Tutorial ViewModels : A Simple Example
Based on #Fletcher Johns answer I came up with:
custom layout
can inflate from XML
is able to save/restore direct and indirect children. I improved #Fletcher Johns' answer to save the ids in String->Id map instead of IntArray.
the only small drawback is that you must declare your saveable child views beforehand.
open class AddressView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
protected lateinit var countryInputLayout: TextInputLayout
protected lateinit var countryAutoCompleteTextView: CountryAutoCompleteTextView
protected lateinit var cityInputLayout: TextInputLayout
protected lateinit var cityEditText: CityEditText
protected lateinit var postCodeInputLayout: TextInputLayout
protected lateinit var postCodeEditText: PostCodeEditText
protected lateinit var streetInputLayout: TextInputLayout
protected lateinit var streetEditText: StreetEditText
init {
initView()
}
private fun initView() {
val view = inflate(context, R.layout.view_address, this)
orientation = VERTICAL
countryInputLayout = view.findViewById(R.id.countryInputLayout)
countryAutoCompleteTextView = view.findViewById(R.id.countryAutoCompleteTextView)
streetInputLayout = view.findViewById(R.id.streetInputLayout)
streetEditText = view.findViewById(R.id.streetEditText)
cityInputLayout = view.findViewById(R.id.cityInputLayout)
cityEditText = view.findViewById(R.id.cityEditText)
postCodeInputLayout = view.findViewById(R.id.postCodeInputLayout)
postCodeEditText = view.findViewById(R.id.postCodeEditText)
}
// Declare your direct and indirect child views that need to be saved
private val childrenToSave get() = mapOf<String, View>(
"coutryIL" to countryInputLayout,
"countryACTV" to countryAutoCompleteTextView,
"streetIL" to streetInputLayout,
"streetET" to streetEditText,
"cityIL" to cityInputLayout,
"cityET" to cityEditText,
"postCodeIL" to postCodeInputLayout,
"postCodeET" to postCodeEditText,
)
private var viewIds: HashMap<String, Int>? = null
override fun onSaveInstanceState(): Parcelable? {
// Create a bundle to put super parcelable in
val bundle = Bundle()
bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState())
// Store viewIds in the bundle - initialize if necessary.
if (viewIds == null) {
childrenToSave.values.forEach { view -> view.id = generateViewId() }
viewIds = HashMap<String, Int>(childrenToSave.mapValues { (key, view) -> view.id })
}
bundle.putSerializable(STATE_VIEW_IDS, viewIds)
return bundle
}
override fun onRestoreInstanceState(state: Parcelable?) {
// We know state is a Bundle:
val bundle = state as Bundle
// Get mViewIds out of the bundle
viewIds = bundle.getSerializable(STATE_VIEW_IDS) as HashMap<String, Int>
// For each id, assign to the view of same index
if (viewIds != null) {
viewIds!!.forEach { (key, id) -> childrenToSave[key]!!.id = id }
}
super.onRestoreInstanceState(bundle.getParcelable(SUPER_INSTANCE_STATE))
}
companion object {
private const val SUPER_INSTANCE_STATE = "saved_instance_state_parcelable"
private const val STATE_VIEW_IDS = "state_view_ids"
}
}

Categories

Resources