可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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
.
回答2:
There are many related problems with a similar error message. Check the second line of this particular stack trace. This exception is specifically related to the call to FragmentManagerImpl.popBackStackImmediate
.
This method call, like popBackStack
, will always fail with IllegalArgumentException
if the session state has already been saved. Check the source. There is nothing you can do to stop this exception being thrown.
- Removing the call to
super.onSaveInstanceState
will not help. - Creating the Fragment with
commitAllowingStateLoss
will not help.
Here's how I observed the problem:
- There's a form with a submit button.
- When the button is clicked a dialog is created and an async process starts.
- The user clicks the home key before the process is finished -
onSaveInstanceState
is called. - The process completes, a callback is made and
popBackStackImmediate
is attempted. IllegalStateException
is thrown.
Here's what I did to solve it:
As it is not possible to avoid the IllegalStateException
in the callback, catch & ignore it.
try { activity.getSupportFragmentManager().popBackStackImmediate(name); } catch (IllegalStateException ignored) { // There's no way to avoid getting this if saveInstanceState has already been called. }
This is enough to stop the app from crashing. But now the user will restore the app and see that the button they thought they'd pressed hasn't been pressed at all (they think). The form fragment is still showing!
To fix this, when the dialog is created, make some state to indicate the process has started.
progressDialog.show(fragmentManager, TAG); submitPressed = true;
And save this state in the bundle.
@Override public void onSaveInstanceState(Bundle outState) { ... outState.putBoolean(SUBMIT_PRESSED, submitPressed); }
Don't forget to load it back again in onViewCreated
Then, when resuming, rollback the fragments if submit was previously attempted. This prevents the user from coming back to what seems like an un-submitted form.
@Override public void onResume() { super.onResume(); if (submitPressed) { // no need to try-catch this, because we are not in a callback activity.getSupportFragmentManager().popBackStackImmediate(name); } }
回答3:
Check if the activity isFinishing()
before showing the fragment and pay attention to commitAllowingStateLoss()
.
Example:
if(!isFinishing()) { FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); DummyFragment dummyFragment = DummyFragment.newInstance(); ft.add(R.id.dummy_fragment_layout, dummyFragment); ft.commitAllowingStateLoss(); }
回答4:
Here is a different solution to this problem.
Using a private member variable you are able to set the returned data as an intent that can then be processed after super.onResume();
Like so:
private Intent mOnActivityResultIntent = null; @Override protected void onResume() { super.onResume(); if(mOnActivityResultIntent != null){ ... do things ... mOnActivityResultIntent = null; } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data){ if(data != null){ mOnActivityResultIntent = data; } }
回答5:
Short And working Solution :
Follow Simple Steps
Steps
Step 1 : Override onSaveInstanceState
state in respective fragment. And remove super method from it.
@Override public void onSaveInstanceState( Bundle outState ) { }
Step 2 : Use fragmentTransaction.commitAllowingStateLoss( );
instead of fragmentTransaction.commit( );
while fragment operations.
回答6:
BEWARE, using transaction.commitAllowingStateLoss()
could result in a bad experience for the user. For more information on why this exception is thrown, see this post.
回答7:
I found a dirty solution for this kind of problem. If you still want to keep your ActivityGroups
for whatever reason (I had time limitation reasons), you just implement
public void onBackPressed() {}
in your Activity
and do some back
code in there. even if there is no such Method on older Devices, this Method gets called by newer ones.
回答8:
It's October 2017, and Google makes Android Support Library with the new things call Lifecycle component. It provides some new idea for this 'Can not perform this action after onSaveInstanceState' problem.
In short:
- Use lifecycle component to determine if it's correct time for popping up your fragment.
Longer version with explain:
why this problem come out?
It's because you are trying to use FragmentManager
from your activity(which is going to hold your fragment I suppose?) to commit a transaction for you fragment. Usually this would look like you are trying to do some transaction for an up coming fragment, meanwhile the host activity already call savedInstanceState
method(user may happen to touch the home button so the activity calls onStop()
, in my case it's the reason)
Usually this problem shouldn't happen -- we always try to load fragment into activity at the very beginning, like the onCreate()
method is a perfect place for this. But sometimes this do happen, especially when you can't decide what fragment you will load to that activity, or you are trying to load fragment from an AsyncTask
block(or anything will take a little time). The time, before the fragment transaction really happens, but after the activity's onCreate()
method, user can do anything. If user press the home button, which triggers the activity's onSavedInstanceState()
method, there would be a can not perform this action
crash.
If anyone want to see deeper in this issue, I suggest them to take a look at this blog post. It looks deep inside the source code layer and explain a lot about it. Also, it gives the reason that you shouldn't use the commitAllowingStateLoss()
method to workaround this crash(trust me it offers nothing good for your code)
How to fix this?
Should I use commitAllowingStateLoss()
method to load fragment? Nope you shouldn't;
Should I override onSaveInstanceState
method, ignore super
method inside it? Nope you shouldn't;
Should I use the magical isFinishing
inside activity, to check if the host activity is at the right moment for fragment transaction? Yeah this looks like the right way to do.
Take a look at what Lifecycle component can do.
Basically, Google makes some implementation inside the AppCompatActivity
class(and several other base class you should use in your project), which makes it a easier to determine current lifecycle state. Take a look back to our problem: why would this problem happen? It's because we do something at the wrong timing. So we try not to do it, and this problem will be gone.
I code a little for my own project, here is what I do using LifeCycle
. I code in Kotlin.
val hostActivity: AppCompatActivity? = null // the activity to host fragments. It's value should be properly initialized. fun dispatchFragment(frag: Fragment) { hostActivity?.let { if(it.lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)){ showFragment(frag) } } } private fun showFragment(frag: Fragment) { hostActivity?.let { Transaction.begin(it, R.id.frag_container) .show(frag) .commit() }
As I show above. I will check the lifecycle state of the host activity. With Lifecycle component within support library, this could be more specific. The code lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)
means, if current state is at least onResume
, not later than it? Which makes sure my method won't be execute during some other life state(like onStop
).
Is it all done?
Of course not. The code I have shown tells some new way to prevent application from crashing. But if it do go to the state of onStop
, that line of code wont do things and thus show nothing on your screen. When users come back to the application, they will see an empty screen, that's the empty host activity showing no fragments at all. It's bad experience(yeah a little bit better than a crash).
So here I wish there could be something nicer: app won't crash if it comes to life state later than onResume
, the transaction method is life state aware; besides, the activity will try continue to finished that fragment transaction action, after the user come back to our app.
I add something more to this method:
class FragmentDispatcher(_host: FragmentActivity) : LifecycleObserver { private val hostActivity: FragmentActivity? = _host private val lifeCycle: Lifecycle? = _host.lifecycle private val profilePendingList = mutableListOf() @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun resume() { if (profilePendingList.isNotEmpty()) { showFragment(profilePendingList.last()) } } fun dispatcherFragment(frag: BaseFragment) { if (lifeCycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true) { showFragment(frag) } else { profilePendingList.clear() profilePendingList.add(frag) } } private fun showFragment(frag: BaseFragment) { hostActivity?.let { Transaction.begin(it, R.id.frag_container) .show(frag) .commit() } } }
I maintain a list inside this dispatcher
class, to store those fragment don't have chance to finish the transaction action. And when user come back from home screen and found there is still fragment waiting to be launched, it will go to the resume()
method under the @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
annotation. Now I think it should be working like I expected.
回答9:
I had a similar problem, the scenario was like this:
- My Activity is adding/replacing list fragments.
- Each list fragment has a reference to the activity, to notify the activity when a list item is clicked (observer pattern).
- Each list fragment calls setRetainInstance(true); in its onCreate method.
The onCreate method of the activity was like this:
mMainFragment = (SelectionFragment) getSupportFragmentManager() .findFragmentByTag(MAIN_FRAGMENT_TAG); if (mMainFragment == null) { mMainFragment = new SelectionFragment(); mMainFragment.setListAdapter(new ArrayAdapter(this, R.layout.item_main_menu, getResources().getStringArray( R.array.main_menu))); mMainFragment.setOnSelectionChangedListener(this); FragmentTransaction transaction = getSupportFragmentManager() .beginTransaction(); transaction.add(R.id.content, mMainFragment, MAIN_FRAGMENT_TAG); transaction.commit(); }
The exception was thrown because the when configuration changes (device rotated), the activity is created, the main fragment is retrieved from the history of the fragment manager and at the same time the fragment already has an OLD reference to the destroyed activity
changing the implementation to this solved the problem:
mMainFragment = (SelectionFragment) getSupportFragmentManager() .findFragmentByTag(MAIN_FRAGMENT_TAG); if (mMainFragment == null) { mMainFragment = new SelectionFragment(); mMainFragment.setListAdapter(new ArrayAdapter(this, R.layout.item_main_menu, getResources().getStringArray( R.array.main_menu))); FragmentTransaction transaction = getSupportFragmentManager() .beginTransaction(); transaction.add(R.id.content, mMainFragment, MAIN_FRAGMENT_TAG); transaction.commit(); } mMainFragment.setOnSelectionChangedListener(this);
you need to set your listeners each time the activity is created to avoid the situation where the fragments have references to old destroyed instances of the activity.
回答10:
Do not use commitAllowingStateLoss(), it should only be used for cases where it is okay for the UI state to change unexpectedly on the user.
https://developer.android.com/reference/android/app/FragmentTransaction.html#commitAllowingStateLoss()
Instead, use if (fragment.isResume()) check outside the operation you met this IllegalStateException "Can not perform this action after onSaveInstanceState"
回答11:
I was getting this exception when i was pressing back button to cancel intent chooser on my map fragment activity. I resolved this by replacing the code of onResume(where i was initializing the fragment) to onstart() and the app is working fine.Hope it helps.
回答12:
I think using transaction.commitAllowingStateLoss();
is not best solution. This exception will be thrown when activity's configuration changed and fragment onSavedInstanceState()
is called and thereafter your async callback method tries to commit fragment.
Simple solution could be check whether activity is changing configuration or not
e.g. check isChangingConfigurations()
i.e.
if(!isChangingConfigurations()) { //commit transaction. }
Checkout this link as well
回答13:
Possibly the smoothest and the simplest solution I found in my case was to avoid popping the offending fragment off the stack in response to activity result. So changing this call in my onActivityResult()
:
popMyFragmentAndMoveOn();
to this:
new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { popMyFragmentAndMoveOn(); } }
helped in my case.
回答14:
Whenever you are trying to load a fragment in your activity make sure that activity is in resume and not going to pause state.In pause state you may end up losing commit operation that is done.
You can use transaction.commitAllowingStateLoss() instead of transaction.commit() to load fragment
or
Create a boolean and check if activity is not going to onpause
@Override public void onResume() { super.onResume(); mIsResumed = true; } @Override public void onPause() { mIsResumed = false; super.onPause(); }
then while loading fragment check
if(mIsResumed){ //load the your fragment }
回答15:
Add this in your activity
@Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (outState.isEmpty()) { // Work-around for a pre-Android 4.2 bug outState.putBoolean("bug:fix", true); } }
回答16:
Starting from support library version 24.0.0 you can call FragmentTransaction.commitNow()
method which commits this transaction synchronously instead of calling commit()
followed by executePendingTransactions()
. As documentation says this approach even better:
Calling commitNow is preferable to calling commit() followed by executePendingTransactions() as the latter will have the side effect of attempting to commit all currently pending transactions whether that is the desired behavior or not.
回答17:
I have also experienced this issue and problem occurs every time when context of your FragmentActivity
gets changed (e.g. Screen orientation is changed, etc.). So the best fix for it is to update context from your FragmentActivity
.
回答18:
The exception is threw here (In FragmentActivity):
@Override public void onBackPressed() { if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) { super.onBackPressed(); } }
In FragmentManager.popBackStatckImmediate()
,FragmentManager.checkStateLoss()
is called firstly. That's the cause of IllegalStateException
. See the implementation below:
private void checkStateLoss() { if (mStateSaved) { // Boom! throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } if (mNoTransactionsBecause != null) { throw new IllegalStateException( "Can not perform this action inside of " + mNoTransactionsBecause); } }
I solve this problem simply by using a flag to mark Activity's current status. Here's my solution:
public class MainActivity extends AppCompatActivity { /** * A flag that marks whether current Activity has saved its instance state */ private boolean mHasSaveInstanceState; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onSaveInstanceState(Bundle outState) { mHasSaveInstanceState = true; super.onSaveInstanceState(outState); } @Override protected void onResume() { super.onResume(); mHasSaveInstanceState = false; } @Override public void onBackPressed() { if (!mHasSaveInstanceState) { // avoid FragmentManager.checkStateLoss()'s throwing IllegalStateException super.onBackPressed(); } }
}
回答19:
I ended up with creating a base fragment and make all fragments in my app extend it
public class BaseFragment extends Fragment { private boolean mStateSaved; @CallSuper @Override public void onSaveInstanceState(Bundle outState) { mStateSaved = true; super.onSaveInstanceState(outState); } /** * Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException * would otherwise occur. */ public void showAllowingStateLoss(FragmentManager manager, String tag) { // API 26 added this convenient method if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (manager.isStateSaved()) { return; } } if (mStateSaved) { return; } show(manager, tag); } }
Then when I try to show a fragment I use showAllowingStateLoss
instead of show
like this:
MyFragment.newInstance() .showAllowingStateLoss(getFragmentManager(), MY_FRAGMENT.TAG);
I came up to this solution from this PR: https://github.com/googlesamples/easypermissions/pull/170/files
回答20:
Another possible workaround, which I'm not sure if helps in all cases (origin here) :
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { final View rootView = findViewById(android.R.id.content); if (rootView != null) { rootView.cancelPendingInputEvents(); } } }