I have extended the MaterialButton to display a consistent loading state on the button when clicked however I ran into a minor issue when implementing SavedState restoration.
The progress button variant of this I have written, disables the view, displays an animated loading spinner, optionally displays loading text and restores then restores the previously displayed text/state when the view is enabled.
MaterialButton.SavedState is package-private so cannot be extended externally. This does not create a problem with my implementation as I don't use the 'checked' field it extends but it does raise these questions:
TLDR
Is it wrong to extend this MaterialButton?
If not would it be appropriate to submit a PR to the public repo to make SavedState public?
material-components-android:1.2.0
Is it wrong to extend this MaterialButton?
The MaterialButton is not a final class then you can extend it, and for example the ExtendedFloatingActionButton extends it.
If not would it be appropriate to submit a PR to the public repo to make SavedState public?
You don't need to make MaterialButton.SavedState public. You can do something like:
public class MyButton extends MaterialButton
{
private String text;
//....
static class SavedState extends AbsSavedState {
#Nullable CharSequence myText;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(#NonNull Parcel source, ClassLoader loader) {
super(source, loader);
myText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
}
#Override
public void writeToParcel(#NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
TextUtils.writeToParcel(myText, dest, flags);
}
#NonNull
#Override
public String toString() {
return "MyButton.SavedState{"
+ " text="
+ myText
+ "}";
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
#NonNull
#Override
public SavedState createFromParcel(#NonNull Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
#Nullable
#Override
public SavedState createFromParcel(#NonNull Parcel in) {
return new SavedState(in, null);
}
#NonNull
#Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
#Nullable
#Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.myText = text;
return ss;
}
#Override
public void onRestoreInstanceState(#Nullable Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
text = ss.myText.toString();
}
}
In this way the MyButton.SavedState will contain also the MaterialButton.SaveState without extending it.
Related
I am using Parcelable when I am sending data between fragments. I send my object class from 'TasksFragment' to 'EditTaskFragment'. My object class is below.
public class TaskResult extends BaseObservable implements Parcelable {
private int taskID;
private String taskName;
public TaskResult() {
}
public int getTaskID() {
return taskID;
}
public void setTaskID(int taskID) {
this.taskID = taskID;
}
#Bindable
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
notifyPropertyChanged(BR.taskName);
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.taskID);
dest.writeString(this.taskName);
}
protected TaskResult(Parcel in) {
this.taskID = in.readInt();
this.taskName = in.readString();
}
public static final Creator<TaskResult> CREATOR = new Creator<TaskResult>() {
#Override
public TaskResult createFromParcel(Parcel source) {
return new TaskResult(source);
}
#Override
public TaskResult[] newArray(int size) {
return new TaskResult[size];
}
};
}
I get the data like this in 'EditTaskFragment';
mViewModel.taskResult =
getArguments().getParcelable(Keys.KEY_TASK_RESULT);
For example when I updated an any data in TaskResult class which I get, the data which in TasksFragment updated, too.
This value updated in TasksFragment, too. But I don't it. How do I?
If I clone this class, is it mind?
Why do you send your data by the fragment arguments when you are using the viewmodel?
The viewmodel is the best solution to share any data between the multiple fragments.
By anyway, I assume that you use the viewmodel in both fragments based on the one activity. Reason of your problem is that the instance of the viewmodel is equal in your fragments.
because I don't like the default implementation of Android's Bottom Navigation Bar, I have made my own and connected it with a FragmentStatePagerAdapter. I just want to know how I can get already created Fragments with their saved instance state from my adapter just like viewpager does?
It would be nice if anyone could help me solve this.
I solved it on my own. I totally had forgotten to care about the Saved Instance State of my view. I got it from the ViewPager source code and modified it slightly because some Methods were marked as deprecated. Here the implementation.
#Nullable
#Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SaveState ss = new SaveState(superState);
ss.position = lastSelectedPosition;
ss.itemId = lastSelectedItemId;
if (mAdapter != null) {
ss.adapterState = mAdapter.saveState();
}
return ss;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SaveState)) {
super.onRestoreInstanceState(state);
return;
}
SaveState ss = (SaveState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (mAdapter != null) {
mAdapter.restoreState(ss.adapterState, ss.loader);
setSelectedItem(ss.position, ss.itemId);
lastSelectedItemId = ss.itemId;
lastSelectedPosition = ss.position;
}
}
public static class SaveState extends BaseSavedState {
int position;
int itemId;
Parcelable adapterState;
ClassLoader loader;
SaveState(Parcelable superState) {
super(superState);
}
#Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(position);
out.writeInt(itemId);
out.writeParcelable(adapterState, flags);
}
public static final Parcelable.ClassLoaderCreator<SaveState> CREATOR = new
Class later dater<SaveState>() {
#Override
public SaveState createFromParcel(Parcel source) {
return new SaveState(source, null);
}
#Override
public SaveState[] newArray(int size) {
return new SaveState[size];
}
#Override
public SaveState createFromParcel(Parcel source, ClassLoader loader) {
return new SaveState(source, loader);
}
};
SaveState(Parcel in, ClassLoader loader) {
super(in);
if (loader == null) {
loader = getClass().getClassLoader();
}
position = in.readInt();
itemId = in.readInt();
adapterState = in.readParcelable(loader);
this.loader = loader;
}
}
I have 2 classes ParentClass and ChildClass:
ParentClass is a subclass of RelativeLayout which saves state using onSaveInstanceState() / onRestoreInstanceState() with following code:
#Override
protected Parcelable onSaveInstanceState() {
SavedState savedState = new SavedState(super.onSaveInstanceState());
savedState.someObj = someObj;
savedState.someInt = someInt;
savedState.someEnum = someEnum;
return savedState;
}
#Override
protected void onRestoreInstanceState(Parcelable in) {
if (!(in instanceof SavedState)) {
super.onRestoreInstanceState(in);
return;
}
SavedState savedState = (SavedState) in;
super.onRestoreInstanceState(savedState.getSuperState());
this.someObj = savedState.someObj;
this.someInt = savedState.someInt;
this.someEnum = savedState.someEnum;
}
And here is the SavedState code:
public static class SavedState extends BaseSavedState {
private SomeObj someObj;
private int someInt;
private SomeEnum someEnum;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
someInt = source.readInt();
someEnum = SomeEnum.values()[source.readInt()];
someObj = source.readParcelable(SomeObj.class.getClassLoader());
}
#Override
public void writeToParcel(#NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(someInt);
dest.writeInt(someEnum.ordinal());
dest.writeParcelable(someObj, flags);
}
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
Nothing fancy, everything works as expected (saving \ restoring state) when I use this view directly.
Then I have ChildClass which extends ParentClass It also saves it state using exactly same code, except that I am saving only long and int fields.
When the app tries to restore ChildClass view state by calling it SavedState constructor with Parcel from CREATOR it crashes with following stack:
Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.company.app.ParentClass$SavedState
1 android.os.Parcel.readParcelableCreator Parcel.java, line 2320
2 android.os.Parcel.readParcelable Parcel.java, line 2270
3 android.view.AbsSavedState.<init> AbsSavedState.java, line 57
4 android.view.View$BaseSavedState.<init> View.java, line 20128
5 com.company.app.ChildClass$SavedState.<init> ChildClass.java, line 151
6 com.company.app.ChildClass$SavedState$1.a ChildClass.java, line 166
7 com.company.app.ChildClass$SavedState$1.createFromParcel ChildClass.java, line 164
Make your SavedState class implement Parcelable
public static class SavedState extends Parcelable {
I have a layout in which I have dynamically added custom views at a push of a button. These layouts extend LinearLayout and each carry their own unique Action objects.
The views will disappear, however, if onCreate is called again, when the user navigates away or rotates the screen. I want to keep these custom ActionHolder views there. To add to the problem, the ActionHolder objects contain sensitive information. The Action objects themselves store a live timer(that is supposed to keep on ticking even if the app is off), as well as other information.
According to an answer below, I have done the following, but to no avail. Here is what I have so far:
public class ActionHolder extends LinearLayout implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2271402255369440088L;
private Action action;
private String timer;
public static final int ACTION_TITLE = 0, ACTION_TIMER = 1,
PAUSEANDPLAY_BTN = 2, FINISH_BTN = 3;
public ActionHolder(Context context) {
super(context);
}
public ActionHolder(Context context, AttributeSet attr) {
super(context, attr);
}
public ActionHolder(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
}
public void initiate(Action input) {
// int hashedID = input.getActionName().hashCode();
// if (hashedID < 0)
// hashedID *= -1;
// this.setId(hashedID);
this.setOrientation(LinearLayout.VERTICAL);
this.setLayoutParams(new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
action = input;
LayoutInflater inflater = LayoutInflater.from(getContext());
View view = inflater.inflate(R.layout.action_holder_layout, this, true);
TextView actionTitle = (TextView) view
.findViewById(com.tonimiko.mochi_bean.R.id.action_holder_title);
actionTitle.setText(action.getActionName());
actionTitle.setId(ActionHolder.ACTION_TITLE);
TextView actionTimer = (TextView) view
.findViewById(R.id.action_holder_timer);
actionTimer.setId(ActionHolder.ACTION_TIMER);
Button pauseBtn = (Button) view
.findViewById(com.tonimiko.mochi_bean.R.id.pause_and_play_timer_btn);
pauseBtn.setId(ActionHolder.PAUSEANDPLAY_BTN);
Button finishBtn = (Button) view
.findViewById(com.tonimiko.mochi_bean.R.id.finish_activity_button);
finishBtn.setId(ActionHolder.FINISH_BTN);
action.setActivityStartTime();
}
public Action finishAction() {
action.setActivityStopTime();
return action;
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
public String toString() {
return "Action stored: " + action.getActionName();
}
#Override
public boolean equals(Object other) {
ActionHolder otherObj = (ActionHolder) other;
if (this.action.getActionName().toUpperCase()
.equals(otherObj.action.getActionName().toUpperCase()))
return true;
return false;
}
#Override
public int hashCode() {
return action.getActionName().hashCode();
}
#Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
Bundle data = new Bundle();
data.putString("Timer", timer);
data.putSerializable("Action", action);
Log.e("debug", "View onSaveInstanceState called!"); // TODO
Parcelable test = new ActionHolderSavedState(superState, data);
if(test==null)
Log.e("debug", "NULL PARCELABLE"); // TODO
return new ActionHolderSavedState(superState, data);
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
Log.e("debug", "View onRestore called!");
if (state instanceof ActionHolderSavedState) {
final ActionHolderSavedState savedState = (ActionHolderSavedState) state;
this.action = savedState.getAction();
this.timer = savedState.getTimer();
// this.initiate(action);
super.onRestoreInstanceState(savedState.getSuperState());
Log.e("debug", "View onRestoreInstanceState finished"); // TODO
}
}
static class ActionHolderSavedState extends BaseSavedState {
private Action storedAction;
private String storedTimer;
public ActionHolderSavedState(Parcelable superState, Bundle data) {
super(superState);
storedTimer = data.getString("Timer");
storedAction = (Action) data.getSerializable("Action");
}
private ActionHolderSavedState(Parcel in) {
super(in);
storedTimer = in.readString();
storedAction = in.readParcelable(ActionHolder.class.getClassLoader());
}
public Action getAction() {
return storedAction;
}
public String getTimer() {
return storedTimer;
}
#Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeString(storedTimer);
out.writeSerializable(storedAction);
}
// required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<ActionHolderSavedState> CREATOR = new Parcelable.Creator<ActionHolderSavedState>() {
public ActionHolderSavedState createFromParcel(final Parcel in) {
return new ActionHolderSavedState(in);
}
public ActionHolderSavedState[] newArray(int size) {
return new ActionHolderSavedState[size];
}
};
}
}
Is there SOMETHING I am doing wrong? I've spend almost 4 days already on this.
I have a situation very similar to yours, with custom views being added dynamically to the screen and that need to save state when the activity is killed by the OS and recreated later, for example.
I'm overriding onSaveInstanceState on the custom view. It needs to return a Parcelable object. The key is to create a custom class that extends BaseSavedState and stores your data into that Parcelable. It would look somewhat like this:
#Override
protected Parcelable onSaveInstanceState() {
final Parcelable state = super.onSaveInstanceState();
return new ContainerLayoutSavedState(state, data);
}
#Override
protected void onRestoreInstanceState(final Parcelable state) {
if (state instanceof ContainerLayoutSavedState) {
final ContainerLayoutSavedState savedState = (ContainerLayoutSavedState)state;
this.data = savedState.getData();
super.onRestoreInstanceState(savedState.getSuperState());
}
}
public static class ContainerLayoutSavedState extends BaseSavedState {
private String data;
ContainerLayoutSavedState(final Parcelable superState, final String data) {
super(superState);
// Here in this constructor you inject whatever you want to get saved into the Parcelable object. In this contrived example, we're just saving a string called data.
this.data = data;
}
private ContainerLayoutSavedState(final Parcel in) {
super(in);
data = in.readString();
}
public String getData()
return data;
}
#Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeString(data);
}
// required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<ContainerLayoutSavedState> CREATOR = new Parcelable.Creator<ContainerLayoutSavedState>() {
#Override
public ContainerLayoutSavedState createFromParcel(final Parcel in) {
return new ContainerLayoutSavedState(in);
}
#Override
public ContainerLayoutSavedState[] newArray(final int size) {
return new ContainerLayoutSavedState[size];
}
};
} }
Also, don't forget to set IDs to your dynamically added views, so they get re-added to the View tree when you come back.
Assume you want to derive your own View class from an existing View implementation, adding a bit of value, hence maintaining a few variables which represent your View's state in a meaningful way.
It would be nice if your View would save its state automatically just like others do (if an ID is assigned) so you would want to override onRestoreInstanceState() and onSaveInstanceState().
Of course, you need to call the respective methods of your base class, and you need to combine your state information with that of your base class.
Obviously, the only safe way to do so is to wrap your super class' Parcelable in an own Parcelable such that the keys won't get mixed up.
Now there's View.BaseSavedState and its interesting getSuperState() method but I somehow fail to understand how this really adds value to just storing the base class' Parcelable in a Bundle along with the derived View's state values and return that. On the other hand, maybe some other system component will expect all InstanceState information to be of type View.AbsSavedState (e.g. such that getSuperState() can be called)?
Any experiences you're willing to share?
To complement James Chen's answer, here is a full example of how to use this method, based on blog article by Charles Harley.
Code from the link:
public class LockCombinationPicker extends LinearLayout {
private NumberPicker numberPicker1;
private NumberPicker numberPicker2;
private NumberPicker numberPicker3;
public LockCombinationPicker(Context context) {
this(context, null);
}
public LockCombinationPicker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LockCombinationPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
loadViews();
}
private void loadViews() {
LayoutInflater.from(getContext()).inflate(R.layout.lock_combination_picker, this, true);
numberPicker1 = (NumberPicker) findViewById(R.id.number1);
numberPicker1.setMinValue(0);
numberPicker1.setMaxValue(10);
numberPicker2 = (NumberPicker) findViewById(R.id.number2);
numberPicker2.setMinValue(0);
numberPicker2.setMaxValue(10);
numberPicker3 = (NumberPicker) findViewById(R.id.number3);
numberPicker3.setMinValue(0);
numberPicker3.setMaxValue(10);
}
#Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState, numberPicker1.getValue(), numberPicker2.getValue(), numberPicker3.getValue());
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
numberPicker1.setValue(savedState.getNumber1());
numberPicker2.setValue(savedState.getNumber2());
numberPicker3.setValue(savedState.getNumber3());
}
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// As we save our own instance state, ensure our children don't save and restore their state as well.
super.dispatchFreezeSelfOnly(container);
}
#Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
/** See comment in {#link #dispatchSaveInstanceState(android.util.SparseArray)} */
super.dispatchThawSelfOnly(container);
}
/**
* Convenience class to save / restore the lock combination picker state. Looks clumsy but once created is easy to maintain and use.
*/
protected static class SavedState extends BaseSavedState {
private final int number1;
private final int number2;
private final int number3;
private SavedState(Parcelable superState, int number1, int number2, int number3) {
super(superState);
this.number1 = number1;
this.number2 = number2;
this.number3 = number3;
}
private SavedState(Parcel in) {
super(in);
number1 = in.readInt();
number2 = in.readInt();
number3 = in.readInt();
}
public int getNumber1() {
return number1;
}
public int getNumber2() {
return number2;
}
public int getNumber3() {
return number3;
}
#Override
public void writeToParcel(Parcel destination, int flags) {
super.writeToParcel(destination, flags);
destination.writeInt(number1);
destination.writeInt(number2);
destination.writeInt(number3);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
I think the design needs us, and as the name implies, to implement a subclass of View.BaseSavedState to store values by overriding Parcelable's interface.
TextView.SavedState is a good example
public static class SavedState extends BaseSavedState {
int selStart;
int selEnd;
CharSequence text;
boolean frozenWithFocus;
CharSequence error;
SavedState(Parcelable superState) {
super(superState);
}
#Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(selStart);
out.writeInt(selEnd);
out.writeInt(frozenWithFocus ? 1 : 0);
TextUtils.writeToParcel(text, out, flags);
if (error == null) {
out.writeInt(0);
} else {
out.writeInt(1);
TextUtils.writeToParcel(error, out, flags);
}
}
#Override
public String toString() {
String str = "TextView.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " start=" + selStart + " end=" + selEnd;
if (text != null) {
str += " text=" + text;
}
return str + "}";
}
#SuppressWarnings("hiding")
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];
}
};
private SavedState(Parcel in) {
super(in);
selStart = in.readInt();
selEnd = in.readInt();
frozenWithFocus = (in.readInt() != 0);
text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
if (in.readInt() != 0) {
error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
}
}
}