I receive notifications from Crashlytics about the following crash in my Xamarin.Forms project:
Fatal Exception: java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.xxx.xxx/xxxxx.MainActivity}:
java.lang.ClassCastException: android.view.AbsSavedState$1 cannot be cast to
android.widget.CompoundButton$SavedState
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2957)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3032)
at android.app.ActivityThread.-wrap11(Unknown Source)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Caused by java.lang.ClassCastException:
android.view.AbsSavedState$1 cannot be cast to android.widget.CompoundButton$SavedState
at android.widget.CompoundButton.onRestoreInstanceState(CompoundButton.java:619)
at android.view.View.dispatchRestoreInstanceState(View.java:18884)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3936)
at android.view.View.restoreHierarchyState(View.java:18862)
at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2248)
at android.app.Activity.onRestoreInstanceState(Activity.java:1153)
at android.app.Activity.performRestoreInstanceState(Activity.java:1108)
at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1266)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2930)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3032)
at android.app.ActivityThread.-wrap11(Unknown Source)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
CompoundButton
is a base class for Switch
and I've got two switches on my main page.SwitchRenderer
and its base classes and I don't see any state preservation code either.In many questions on Stack Overflow it is claimed that the issue might be caused by duplicated android:id
, however as I mentioned above, I don't have custom layouts.
Update
I decided to go deeper with investigation and I started verifying whole state preservation mechanism. Below are my findings:
(viewId, state)
. Also it turned out that all views preserve state as AbsSavedState
only CompoundButton
stores CompoundButton.SavedState
. Therefore my guess is that somehow incorrect state was used to restore CompoundButton
. Sample state: {Bundle[{ android:viewHierarchyState=Bundle[{android:views= {1=android.view.AbsSavedState$1@e738983,2=android.view.AbsSavedState$1@e738983, 3=android.view.AbsSavedState$1@e738983, 4=android.view.AbsSavedState$1@e738983, 5=android.view.AbsSavedState$1@e738983, 6=android.view.AbsSavedState$1@e738983, 7=android.view.AbsSavedState$1@e738983, 8=android.view.AbsSavedState$1@e738983, 9=android.view.AbsSavedState$1@e738983, 10=android.view.AbsSavedState$1@e738983, 11=android.view.AbsSavedState$1@e738983, 12=android.view.AbsSavedState$1@e738983, 13=android.view.AbsSavedState$1@e738983, 14=android.view.AbsSavedState$1@e738983, 15=android.view.AbsSavedState$1@e738983, 16=android.view.AbsSavedState$1@e738983, 17=android.view.AbsSavedState$1@e738983, 18=android.view.AbsSavedState$1@e738983, 19=android.view.AbsSavedState$1@e738983, 20=android.view.AbsSavedState$1@e738983, 21=android.view.AbsSavedState$1@e738983, 22=android.view.AbsSavedState$1@e738983, 23=android.view.AbsSavedState$1@e738983, 24=CompoundButton.SavedState{26e683d checked=false}, 25=android.view.AbsSavedState$1@e738983, 26=CompoundButton.SavedState{8f32832 checked=true}, 27=android.view.AbsSavedState$1@e738983, 28=android.view.AbsSavedState$1@e738983, 29=android.view.AbsSavedState$1@e738983, 30=android.view.AbsSavedState$1@e738983, 31=android.view.AbsSavedState$1@e738983, 32=android.view.AbsSavedState$1@e738983, 33=android.view.AbsSavedState$1@e738983, 34=android.view.AbsSavedState$1@e738983, 35=android.view.AbsSavedState$1@e738983, 36=android.view.AbsSavedState$1@e738983, 37=android.view.AbsSavedState$1@e738983, 16908290=android.view.AbsSavedState$1@e738983, 2131558525=android.view.AbsSavedState$1@e738983, 2131558526=android.view.AbsSavedState$1@e738983}}], android:lastAutofillId=1073741825, android:fragments=android.app.FragmentManagerState@969a700}]}
CompoundButtons
(base class for Switch
) on two pages: MainPage
and modal page. After all I thought that maybe this possible mismatch while restoring state is caused by duplicated ids somehow. I decided to write a piece of code to print whole hierarchy with ids. Below you can see MainPage
and modal page, in total 3 switches. However, there is no duplication here.-- 16908290 - ContentFrameLayout ---- -1 - RelativeLayout ------ -1 - PlatformRenderer -------- 1 - PageRenderer ---------- -1 - DefaultRenderer ------------ -1 - DefaultRenderer -------------- 2 - ImageRenderer ------------ -1 - CustomScrollViewRenderer -------------- -1 - ScrollViewContainer ---------------- -1 - DefaultRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - DefaultRenderer ---------------------- -1 - DefaultRenderer ------------------------ 3 - ImageRenderer ---------------------- 4 - LabelRenderer ---------------------- 5 - LabelRenderer ---------------------- -1 - DefaultRenderer ------------------------ 6 - ImageRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - DefaultRenderer ---------------------- 7 - LabelRenderer ---------------------- 8 - LabelRenderer ---------------------- -1 - DefaultRenderer ------------------------ 9 - ImageRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - DefaultRenderer ---------------------- -1 - DefaultRenderer ------------------------ -1 - GaugeChartRenderer ------------------------ 10 - LabelRenderer ------------------------ 11 - LabelRenderer ------------------------ -1 - GaugeChartRenderer ------------------------ 12 - LabelRenderer ------------------------ 13 - LabelRenderer ------------------ -1 - DefaultRenderer -------------------- 14 - LabelRenderer -------------------- 15 - LabelRenderer ------------------ -1 - LinearChartRenderer -------------------- 16 - LinearChart ------------------ -1 - DefaultRenderer -------------------- -1 - CustomButtonRenderer ---------------------- 17 - Button -------------------- -1 - CustomButtonRenderer ---------------------- 18 - Button -------------------- -1 - CustomButtonRenderer ---------------------- 19 - Button -------------------- -1 - CustomButtonRenderer ---------------------- 20 - Button -------------------- -1 - CustomButtonRenderer ---------------------- 21 - Button -------------------- -1 - CustomButtonRenderer ---------------------- 22 - Button ------------------ -1 - DefaultRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - DefaultRenderer ---------------------- 23 - LabelRenderer ---------------------- 24 - LabelRenderer ---------------------- 25 - LabelRenderer ---------------------- 26 - LabelRenderer ---------------------- 27 - LabelRenderer -------------------- -1 - DefaultRenderer ---------------------- -1 - DefaultRenderer ------------------------ -1 - DefaultRenderer -------------------------- 33 - LabelRenderer -------------------------- 34 - LabelRenderer -------------------------- 35 - LabelRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - CustomSwitchRenderer ---------------------- 28 - Switch -------------------- 29 - LabelRenderer -------------------- -1 - DefaultRenderer ---------------------- 36 - ImageRenderer ------------------ -1 - DefaultRenderer -------------------- -1 - CustomSwitchRenderer ---------------------- 30 - Switch -------------------- 31 - LabelRenderer ------------------ -1 - DefaultRenderer -------------------- 37 - ImageRenderer -------------------- -1 - CustomButtonRenderer ---------------------- 32 - Button -------- 44 - ModalContainer ---------- -1 - View ---------- 38 - PageRenderer ------------ -1 - DefaultRenderer -------------- -1 - DefaultRenderer ---------------- -1 - DefaultRenderer ------------------ 39 - LabelRenderer ------------------ -1 - DefaultRenderer -------------------- 45 - ImageRenderer ---------------- -1 - SearchBarRenderer ------------------ 40 - SearchView -------------------- 16909226 - LinearLayout ---------------------- 16909225 - AppCompatTextView ---------------------- 16909227 - AppCompatImageView ---------------------- 16909229 - LinearLayout ------------------------ 16909231 - AppCompatImageView ------------------------ 16909232 - LinearLayout -------------------------- 16909233 - AutoCompleteTextView -------------------------- 16909228 - AppCompatImageView ------------------------ 16909321 - LinearLayout -------------------------- 16909230 - AppCompatImageView -------------------------- 16909235 - AppCompatImageView -------------- -1 - DefaultRenderer ---------------- -1 - ListViewRenderer ------------------ -1 - SwipeRefreshLayout -------------------- 41 - ListView ---------------------- -1 - Container ---------------------- -1 - Container ------------------------ -1 - DefaultRenderer -------------------- -1 - ImageView -------------- -1 - DefaultRenderer ---------------- -1 - DefaultRenderer ------------------ -1 - CustomSwitchRenderer -------------------- 42 - Switch ------------------ 43 - LabelRenderer
internal static int GenerateViewId() { if ((int)Build.VERSION.SdkInt >= 17) return global::Android.Views.View.GenerateViewId(); if (s_id >= 0x00ffffff) s_id = 0x00000400; return s_id++; } static int s_id = 0x00000400;
It looks fine, unless there is some race condition. I'm running out of ideas.
Update 2
I subclassed Switch
control and overrode OnRestoreSavedInstance
and strange thing that it's never called on my devices. However, OnSaveInstanceState
is called. Please mind that I properly simulated state restoration (it is called in MainActivity
, but doesn't propagate to Switch
).
I found the reason why it behaves in this way. Please take a look at Android's implementation for View.dispatchRestoreState
:
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container)
{
if (mID != NO_ID) {
Parcelable state = container.get(mID); // <--- HERE
if (state != null) {
// Log.i("View", "Restoreing #" + Integer.toHexString(mID)
// + ": " + state);
mPrivateFlags &= ~SAVE_STATE_CALLED;
onRestoreInstanceState(state);
if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onRestoreInstanceState()");
}
}
}
}
Xamarin.Forms sets ids automatically by increasing counter. Therefore after creating page it sets ids from 1
to n
. After another recreation (for example after rotating screen) it sets ids from n+1
to 2n+1
. Therefore none control will be able to restore its state, because when preserving state it will be saved as state for id=x
, however after recreating Activity
this control will have a different id.
Therefore this crash should never occur, because of no state restoration...
Update 3
I noticed also something strange in Android's implementation. CompoundButton
has this implementation:
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setChecked(ss.checked);
requestLayout();
}
However, TextView
(CompoundButton
's ancestor) has this implementation:
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// ...
}
As you can see, TextView
validates first if this cast will be successful, CompoundButton
doesn't. Maybe it's a defect in Android. But still I don't see how it is possible that state has been mismatched and AbsSavedState
has been passed to CompoundButton
instead of CompoundButton.SavedState
.
After all it looks like there must be duplicated id in preserved state, however I don't see any reasonable explanation why. Neither I can reproduce it on my devices. And as I described above:
Xamarin.Forms sets ids automatically by increasing counter. Therefore after creating page it sets ids from
1
ton
. After another recreation (for example after rotating screen) it sets ids fromn+1
to2n+1
. Therefore none control will be able to restore its state, because when preserving state it will be saved as state for id=x, however after recreating Activity this control will have a different id.
Nevertheless I found a workaround to stop crashes.
using Android.Content;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(Switch), typeof(MyApp.Droid.CustomRenderers.CustomSwitchRenderer))]
namespace MyApp.Droid.CustomRenderers
{
public class CustomSwitchRenderer : SwitchRenderer
{
public CustomSwitchRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Switch> e)
{
base.OnElementChanged(e);
if (this.Control != null)
{
this.Control.Id = -1;
this.Control.SaveEnabled = false;
}
}
}
}
It disables state preservation for all Switch
controls. Just in case I also set Id = -1
to override id assigned by Xamarin. -1
is a constant in Android which means "no id".
This workaround doesn't break state preservation in Xamarin.Forms
, because after Page
recreation state relies on your bindings, not Android's mechanism.
However if you would like to make it work without disabling state preservation. You can set some big id which will be constant between runs. Of course you need to set a different ID for each Switch
therefore you may need to create a custom Switch
and add some property like AndroidId
. Note that id should be lower than 0x00ffffff
and big enough to avoid collisions with auto generated ids by Xamarin.
This does not address your overall question, but I believe I can shed some light on your Update 3 section.
First let me re-state your question: why is it that TextView
and CompoundButton
have two different strategies for implementing onRestoreInstanceState()
?
TextView performs conditional logic based on the particular Parcelable
passed to it:
@Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); ... }
While CompoundButton does not:
@Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); ... }
The reason for this is that TextView
and CompoundButton
have two different strategies for implementing onSaveInstanceState()
, and so each class has a corresponding strategy to restore the state.
TextView can return two different types from onSaveInstanceState()
:
@Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); ... if (freezesText || hasSelection) { SavedState ss = new SavedState(superState); ... return ss; } return superState; }
TextView will only return its own custom SavedState
class in situations where the super
call doesn't save everything it needs (i.e. when the TextView has been asked to freeze its text or when it has a selection). In all other cases, it just delegates to the super
call and returns that directly.
Since onRestoreInstanceState()
will receive whatever onSaveInstanceState()
returned, TextView needs to be able to work when it receives either the super
return value or its own SavedState
.
On the other hand, CompoundButton can only return one type from onSaveInstanceState()
:
@Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.checked = isChecked(); return ss; }
Because we know that the passed-in state
object will always be of type SavedState
, we don't have to do any conditional logic. We can just cast it and go.
Hopefully this answer provides a foundation that other answerers can build on, and perhaps eventually answer your primary question.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With