Asked  7 Months ago    Answers:  5   Viewed   66 times

I'm getting user reports from my app in the market, delivering the following exception:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1109)
at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:399)
at android.app.Activity.onBackPressed(Activity.java:2066)
at android.app.Activity.onKeyUp(Activity.java:2044)
at android.view.KeyEvent.dispatch(KeyEvent.java:2529)
at android.app.Activity.dispatchKeyEvent(Activity.java:2274)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1855)
at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1277)
at android.app.Activity.dispatchKeyEvent(Activity.java:2269)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.widget.TabHost.dispatchKeyEvent(TabHost.java:297)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1855)
at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1277)
at android.app.Activity.dispatchKeyEvent(Activity.java:2269)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewRoot.deliverKeyEventPostIme(ViewRoot.java:2880)
at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2853)
at android.view.ViewRoot.handleMessage(ViewRoot.java:2028)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:132)
at android.app.ActivityThread.main(ActivityThread.java:4028)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:491)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:844)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)
at dalvik.system.NativeStart.main(Native Method)

Apparently it has something to do with a FragmentManager, which I don't use. The stacktrace doesn't show any of my own classes, so I have no idea where this exception occurs and how to prevent it.

For the record: I have a tabhost, and in each tab there is a ActivityGroup switching between Activities.

 Answers

12

Please check my answer here. Basically I just had to :

@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

Don't make the call to super() on the saveInstanceState method. This was messing things up...

This is a known bug in the support package.

If you need to save the instance and add something to your outState Bundle you can use the following:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
    super.onSaveInstanceState(outState);
}

In the end the proper solution was (as seen in the comments) to use :

transaction.commitAllowingStateLoss();

when adding or performing the FragmentTransaction that was causing the Exception.

Tuesday, June 1, 2021
 
Xavio
answered 7 Months ago
82

When you rotate the device, Android saves, destroys, and recreates your Activity and its ViewPager of Fragments. Since the ViewPager uses the FragmentManager of your Activity, it saves and reuses those Fragments for you (and does not create new ones), so they will hold the old references to your (now destroyed) original Activity, and you get that IllegalStateException.

In your child Fragments, try something like this:

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    Log.v(TAG, "onAttach");

    // Check if parent activity implements our callback interface
    if (activity != null) {
        try {
            mParentCallback = (Callbacks) activity;
        }
        catch (ClassCastException e) {
        }
    }
}

Then when a selection occurs:

if(mParentCallback != null) {
    mParentCallback.onItemSelected(selectedLink);
}

Since onAttach gets called as part of the Fragment lifecycle, your Fragments will update their callback reference on rotation.

Tuesday, August 10, 2021
 
Gil
answered 4 Months ago
Gil
32

Update 2

React Native

If you can stomach it, use React Native. I know, I know... "dirty web technologies", but in all seriousness, the Android SDK is a disaster, so swallow your pride and just give it a go. You might surprise yourself; I know I did!

Can't or Won't use React Native

No worries, I'd suggest fundamentally changing your approach to networking. Firing a request and running a request handler to update the UI just doesn't work well with Android's component life-cycles.

Instead try one of:

  1. Move to simple message passing system based around LocalBroadcastReceiver and have long-living objects (regular Java classes or Android Services) do your requests and fire events when your app's local state changes. Then in your Activity/Fragment, just listen for certain Intent and update accordingly.
  2. Use a Reactive event library (e.g. RxJava). I've not tried this myself on Android, but had pretty good success using a similar concept library, ReactiveCocoa for a Mac/desktop app. Admittedly these libraries have a fairly steep learning curve, but the approach is quite refreshing once you get used to it.

Update 1: Quick and Dirty (Official) Solution

I believe this is latest official solution from Google. However, the solution really doesn't scale very well. If you're not comfortable messing with queues, handlers and retained instance states yourself then this may be your only option... but don't say I didn't warn you!

Android activities and fragments have support for a LoaderManager which can be used with AsyncTaskLoader. Behind the scenes loader managers are retained in precisely the same way as retained fragments. As such this solution does share a bit in common with my own solution below. AsyncTaskLoader is a partially pre-canned solution that does technically work. However, the API is extremely cumbersome; as I'm sure you'll notice within a few minutes of using it.

My Solution

Firstly, my solution is by no means simple to implement. However, once you get your implementation working it's a breeze to use and you can customise it to your heart's content.

I use a retained fragment that is added to the Activity's fragment manager (or in my case support fragment manager). This is the same technique mentioned in my question. This fragment acts as a provider of sorts which keeps track of which activity it is attached to, and has Message and Runnable (actually a custom sub-class) queues. The queues will execute when the instance state is no longer saved and the corresponding handler (or runnable) is "ready to execute".

Each handler/runnable stores a UUID that refers to a consumer. Consumers are typically fragments (which can be nested safely) somewhere within the activity. When a consumer fragment is attached to an activity it looks for a provider fragment and registers itself using its UUID.

It is important that you use some sort of abstraction, like UUID, instead of referencing consumers (i.e. fragments) directly. This is because fragments are destroyed and recreated often, and you want your callbacks to have a "reference" to new fragments; not old ones that belong to a destroyed activity. As such, unfortunately, you rarely can safely use variables captured by anonymous classes. Again, this is because these variables might refer to an old destroyed fragment or activity. Instead you must ask the provider for the consumer that matches the UUID the handler has stored. You can then cast this consumer to whatever fragment/object it actually is and use it safely, as you know its the latest fragment with a valid Context (the activity).

A handler (or runnable) will be "ready to execute" when the consumer (referred to by UUID) is ready. It is necessary to check if the consumer is ready in addition to the provider because as mentioned in my question, the consumer fragment might believe its instance state is saved even though the provider says otherwise. If the consumer (or provider) are not ready then you put the Message (or runnable) in a queue in the provider.

When a consumer fragment reaches onResume() it informs the provider that it is ready to consume queued messages/runnables. At which point the provider can try execute anything in its queues that belong to the consumer that just became ready.

This results in handlers always executing using a valid Context (the Activity referenced by the provider) and the latest valid Fragment (aka "consumer").

Conclusion

The solution is quite convoluted, however it does work flawlessly once you work out how to implement it. If someone comes up with a simpler solution then I'd be happy to hear it.

Thursday, September 9, 2021
 
Len_D
answered 3 Months ago
67

As i can not comment on your question due to less reputation point. I assume this is your public method to change fragment.

public void beginTransaction(ID id, Bundle bundle)

In this method every time you are adding fragment to activity. So if you are adding fragment first time this will work fine but in case of second fragment you should use replace not add

    fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, fragment).commitAllowingStateLoss();

I think after doing this you should not face this problem. or you can use 'replace' for both first and second fragment. I hope this will help you.

Friday, October 15, 2021
 
Extrakun
answered 2 Months ago
37

Which Android version are you testing this on? ViewPagers use fragments, and because your ViewPager is in a fragment itself, you are nesting the fragments. Nested fragments are only supported from API 17 (Jellybean), so this implementation will only work on devices with Android 4.1+.

Wednesday, November 10, 2021
 
dzirtbry
answered 3 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share