在显示对话框时,我得到“在 onSaveInstanceState 之后无法执行此操作”

一些用户报告,如果他们使用通知栏中的快速行动,他们正在得到一个力量关闭。

我在调用 “ TestDialog”类的通知中显示了一个快速操作。 在 TestDialog 类中,在按下“ nooze”按钮之后,我将显示 SnozeDialog。

private View.OnClickListener btnSnoozeOnClick() {
return new View.OnClickListener() {


public void onClick(View v) {
showSnoozeDialog();
}
};
}


private void showSnoozeDialog() {
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(fm, "snooze_dialog");
}

错误是 *IllegalStateException: Can not perform this action after onSaveInstanceState*.

激发 IllegarStateException 的代码行是:

snoozeDialog.show(fm, "snooze_dialog");

该类正在扩展“ FragmentActivity”,而“ SnoozeDialog”类正在扩展“ DialogFragment”。

下面是错误的完整堆栈跟踪:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1338)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:127)
at com.test.testing.TestDialog.f(TestDialog.java:538)
at com.test.testing.TestDialog.e(TestDialog.java:524)
at com.test.testing.TestDialog.d(TestDialog.java:519)
at com.test.testing.g.onClick(TestDialog.java:648)
at android.view.View.performClick(View.java:3620)
at android.view.View$PerformClick.run(View.java:14292)
at android.os.Handler.handleCallback(Handler.java:605)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4507)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:790)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:557)
at dalvik.system.NativeStart.main(Native Method)

我不能重现这个错误,但是我收到了很多错误报告。

有人能帮我解决这个问题吗?

70020 次浏览

虽然没有任何地方正式提到这个问题,但是我遇到过几次这个问题。根据我的经验,在支持旧平台上的片段的兼容性库中存在一些错误,从而导致了这个问题。您可以使用普通的片段管理器 API 对此进行测试。如果没有工作,那么您可以使用正常的对话框,而不是对话框片段。

请尝试使用 FragmentTransaction 而不是 FragmentManager。我认为下面的代码将解决您的问题。如果没有,请告诉我。

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(ft, "snooze_dialog");

编辑:

零件交易

请检查这个链接。我认为它将解决您的疑问。

如果对话框并不重要(可以不显示它,当应用程序关闭/不在视图中时) ,使用:

boolean running = false;


@Override
public void onStart() {
running = true;
super.onStart();
}


@Override
public void onStop() {
running = false;
super.onStop();
}

并且只在我们运行的时候打开你的对话框(片段) :

if (running) {
yourDialog.show(...);
}

编辑,可能是更好的解决方案:

在生命周期中调用 onSaveInstanceState 是不可预测的,我认为更好的解决方案是像下面这样检查 isSavedInstanceStatedone () :

/**
* True if SavedInstanceState was done, and activity was not restarted or resumed yet.
*/
private boolean savedInstanceStateDone;


@Override
protected void onResume() {
super.onResume();


savedInstanceStateDone = false;
}


@Override
protected void onStart() {
super.onStart();


savedInstanceStateDone = false;
}


protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
savedInstanceStateDone = true;
}




/**
* Returns true if SavedInstanceState was done, and activity was not restarted or resumed yet.
*/
public boolean isSavedInstanceStateDone() {
return savedInstanceStateDone;
}

这意味着在 onSaveInstanceState()之后是 commit()(在 DialogFragment 的情况下是 show())片段。

Android 会将你的片段状态保存在 onSaveInstanceState()。因此,如果你的 commit()片段后,onSaveInstanceState()片段状态将丢失。

因此,如果活动被杀死并在稍后重新创建,那么片段将不会添加到活动中,这是糟糕的用户体验。这就是为什么 Android 不允许不惜一切代价损失状态。

简单的解决方案是检查状态是否已经保存。

boolean mIsStateAlreadySaved = false;
boolean mPendingShowDialog = false;


@Override
public void onResumeFragments(){
super.onResumeFragments();
mIsStateAlreadySaved = false;
if(mPendingShowDialog){
mPendingShowDialog = false;
showSnoozeDialog();
}
}


@Override
public void onPause() {
super.onPause();
mIsStateAlreadySaved = true;
}


private void showSnoozeDialog() {
if(mIsStateAlreadySaved){
mPendingShowDialog = true;
}else{
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(fm, "snooze_dialog");
}
}

注意: onResumeFragments ()将在片段恢复时调用。

这是常见的 问题。 我们通过重写 show ()并在 DialogFragment 扩展类中处理异常来解决这个问题

public class CustomDialogFragment extends DialogFragment {


@Override
public void show(FragmentManager manager, String tag) {
try {
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
} catch (IllegalStateException e) {
Log.d("ABSDIALOGFRAG", "Exception", e);
}
}
}

注意,应用此方法不会改变 DialogFragment.class 的内部字段:

boolean mDismissed;
boolean mShownByMe;

在某些情况下,这可能会导致意外的结果

private void showSnoozeDialog() {
FragmentManager fm = getSupportFragmentManager();
SnoozeDialog snoozeDialog = new SnoozeDialog();
// snoozeDialog.show(fm, "snooze_dialog");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(snoozeDialog, "snooze_dialog");
ft.commitAllowingStateLoss();
}

档号: 链接

几天后,我想分享我的解决方案,我是如何修复它的,为了显示 DialogFragment,你应该覆盖它的 show()方法,并在 Transaction对象上调用 commitAllowingStateLoss()。以下是 Kotlin 的一个例子:

override fun show(manager: FragmentManager?, tag: String?) {
try {
val ft = manager?.beginTransaction()
ft?.add(this, tag)
ft?.commitAllowingStateLoss()
} catch (ignored: IllegalStateException) {


}


}

下面的实现可以用来解决在 Activity生命周期中安全地执行状态更改的问题,特别是在显示对话框时: 如果实例状态已经被保存(例如,由于配置更改) ,它会推迟它们,直到恢复的状态被执行。

public abstract class XAppCompatActivity extends AppCompatActivity {


private String TAG = this.getClass().getSimpleName();


/** The retained fragment for this activity */
private ActivityRetainFragment retainFragment;


/** If true the instance state has been saved and we are going to die... */
private boolean instanceStateSaved;


@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);


// get hold of retain Fragment we'll be using
retainFragment = ActivityRetainFragment.get(this, "Fragment-" + this.getClass().getName());
}


@Override
protected void onPostResume() {
super.onPostResume();


// reset instance saved state
instanceStateSaved = false;


// execute all the posted tasks
for (ActivityTask task : retainFragment.tasks) task.exec(this);
retainFragment.tasks.clear();
}


@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
instanceStateSaved = true;
}


/**
* Checks if the activity state has been already saved.
* After that event we are no longer allowed to commit fragment transactions.
* @return true if the instance state has been saved
*/
public boolean isInstanceStateSaved() {
return instanceStateSaved;
}


/**
* Posts a task to be executed when the activity state has not yet been saved
* @param task The task to be executed
* @return true if the task executed immediately, false if it has been queued
*/
public final boolean post(ActivityTask task)
{
// execute it immediately if we have not been saved
if (!isInstanceStateSaved()) {
task.exec(this);
return true;
}


// save it for better times
retainFragment.tasks.add(task);
return false;
}


/** Fragment used to retain activity data among re-instantiations */
public static class ActivityRetainFragment extends Fragment {


/**
* Returns the single instance of this fragment, creating it if necessary
* @param activity The Activity performing the request
* @param name The name to be given to the Fragment
* @return The Fragment
*/
public static ActivityRetainFragment get(XAppCompatActivity activity, String name) {


// find the retained fragment on activity restarts
FragmentManager fm = activity.getSupportFragmentManager();
ActivityRetainFragment fragment = (ActivityRetainFragment) fm.findFragmentByTag(name);


// create the fragment and data the first time
if (fragment == null) {
// add the fragment
fragment = new ActivityRetainFragment();
fm.beginTransaction().add(fragment, name).commit();
}


return fragment;
}


/** The queued tasks */
private LinkedList<ActivityTask> tasks = new LinkedList<>();


@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);


// retain this fragment
setRetainInstance(true);
}


}


/** A task which needs to be performed by the activity when it is "fully operational" */
public interface ActivityTask {


/**
* Executed this task on the specified activity
* @param activity The activity
*/
void exec(XAppCompatActivity activity);
}
}

然后使用这样的类:

/** AppCompatDialogFragment implementing additional compatibility checks */
public abstract class XAppCompatDialogFragment extends AppCompatDialogFragment {


/**
* Shows this dialog as soon as possible
* @param activity The activity to which this dialog belongs to
* @param tag The dialog fragment tag
* @return true if the dialog has been shown immediately, false if the activity state has been saved
*         and it is not possible to show it immediately
*/
public boolean showRequest(XAppCompatActivity activity, final String tag) {
return showRequest(activity, tag, null);
}


/**
* Shows this dialog as soon as possible
* @param activity The activity to which this dialog belongs to
* @param tag The dialog fragment tag
* @param args The dialog arguments
* @return true if the dialog has been shown immediately, false if the activity state has been saved
*         and it is not possible to show it immediately
*/
public boolean showRequest(XAppCompatActivity activity, final String tag, final Bundle args)
{
return activity.post(new XAppCompatActivity.ActivityTask() {
@Override
public void exec(XAppCompatActivity activity) {
if (args!= null) setArguments(args);
show(activity.getSupportFragmentManager(), tag);
}
});
}


/**
* Dismiss this dialog as soon as possible
* @return true if the dialog has been dismissed immediately, false if the activity state has been saved
*         and it is not possible to dismissed it immediately
*/
public boolean dismissRequest()
{
return dismissRequest(null);
}


/**
* Dismiss this dialog as soon as possible
* @param runnable Actions to be performed before dialog dismissal
* @return true if the dialog has been dismissed immediately, false if the activity state has been saved
*         and it is not possible to dismissed it immediately
*/
public boolean dismissRequest(final Runnable runnable)
{
// workaround as in rare cases the activity could be null
XAppCompatActivity activity = (XAppCompatActivity)getActivity();
if (activity == null) return false;


// post the dialog dismissal
return activity.post(new XAppCompatActivity.ActivityTask() {
@Override
public void exec(XAppCompatActivity activity) {
if (runnable != null) runnable.run();
dismiss();
}
});
}
}

您可以安全地显示对话框,而不必担心应用程序的状态:

public class TestDialog extends XAppCompatDialogFragment {


private final static String TEST_DIALOG = "TEST_DIALOG";


public static void show(XAppCompatActivity activity) {
new TestDialog().showRequest(activity, TEST_DIALOG);
}


public TestDialog() {}


@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity(), R.style.DialogFragmentTheme /* or null as you prefer */)
.setTitle(R.string.title)
// set all the other parameters you need, e.g. Message, Icon, etc.
).create();
}
}

然后从你的 XAppCompatActivity内部调用 TestDialog.show(this)

如果希望创建一个更通用的带参数的对话框类,可以将它们保存在带 show()方法中的参数的 Bundle中,并在 onCreateDialog()中用 getArguments()检索它们。

整个方法可能看起来有点复杂,但是一旦您为活动和对话框创建了两个基类,它就非常容易使用,而且工作得非常好。它可以用于其他基于 Fragment的操作,这些操作可能会受到同样的问题的影响。

  1. 将此类添加到您的项目: (必须在 Android.support. v4.app包中)
package android.support.v4.app;




/**
* Created by Gil on 8/16/2017.
*/


public class StatelessDialogFragment extends DialogFragment {
/**
* Display the dialog, adding the fragment using an existing transaction and then committing the
* transaction whilst allowing state loss.
* * I would recommend you use {@link #show(FragmentTransaction, String)} most of the time but * this is for dialogs you reallly don't care about. (Debug/Tracking/Adverts etc.) * * @param transaction * An existing transaction in which to add the fragment. * @param tag * The tag for this fragment, as per * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. * @return Returns the identifier of the committed transaction, as per * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. * @see StatelessDialogFragment#showAllowingStateLoss(FragmentManager, String) */ public int showAllowingStateLoss(FragmentTransaction transaction, String tag) { mDismissed = false; mShownByMe = true; transaction.add(this, tag); mViewDestroyed = false; mBackStackId = transaction.commitAllowingStateLoss(); return mBackStackId; } /** * Display the dialog, adding the fragment to the given FragmentManager. This is a convenience * for explicitly creating a transaction, adding the fragment to it with the given tag, and * committing it without careing about state. This does not add the transaction to the * back stack. When the fragment is dismissed, a new transaction will be executed to remove it * from the activity.
* * I would recommend you use {@link #show(FragmentManager, String)} most of the time but this is * for dialogs you reallly don't care about. (Debug/Tracking/Adverts etc.) * * * @param manager * The FragmentManager this fragment will be added to. * @param tag * The tag for this fragment, as per * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. * @see StatelessDialogFragment#showAllowingStateLoss(FragmentTransaction, String) */ public void showAllowingStateLoss(FragmentManager manager, String tag) { mDismissed = false; mShownByMe = true; FragmentTransaction ft = manager.beginTransaction(); ft.add(this, tag); ft.commitAllowingStateLoss(); } }
  1. 扩展 状态对话片段而不是 DialogFragment
  2. 使用方法 ShowAllowingStateloss代替 show

  3. 享用;)

许多视图将高级事件(如单击处理程序)发布到事件队列以便延迟运行。所以问题在于已经为 Activity 调用了“ onSaveInstanceState”,但是事件队列包含延迟的“ click event”。因此,当将此事件分派给处理程序时

at android.os.Handler.handleCallback(Handler.java:605)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)

代码执行 show时,将引发 IllegalStateException。

最简单的解决方案是在 onSaveInstanceState中清理事件队列

protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// ..... do some work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
findViewById(android.R.id.content).cancelPendingInputEvents();
}
}

出现此错误似乎是因为输入事件(如 key down 或 onclick 事件)是在调用 onSaveInstanceState之后传递的。

解决方案是在活动中覆盖 onSaveInstanceState并取消任何挂起的事件。

@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();
}
}
}

我遇到这个问题已经好几年了。
互联网上充斥着大量(成百上千?)关于这个问题的讨论,其中的混乱和虚假信息似乎很多。
为了使情况更糟,本着 xkcd“14标准”漫画的精神,我把我的答案扔进了拳击场。
xkcd 14 standards

cancelPendingInputEvents()commitAllowingStateLoss()catch (IllegalStateException e)和类似的解决方案看起来都很糟糕。

希望下面能够简单地展示如何复制和修复这个问题:

private static final Handler sHandler = new Handler();
private boolean mIsAfterOnSaveInstanceState = true;


@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
mIsAfterOnSaveInstanceState = true; // <- To repro, comment out this line
}


@Override
protected void onPostResume()
{
super.onPostResume();
mIsAfterOnSaveInstanceState = false;
}


@Override
protected void onResume()
{
super.onResume();
sHandler.removeCallbacks(test);
}


@Override
protected void onPause()
{
super.onPause();
sHandler.postDelayed(test, 5000);
}


Runnable test = new Runnable()
{
@Override
public void run()
{
if (mIsAfterOnSaveInstanceState)
{
// TODO: Consider saving state so that during or after onPostResume a dialog can be shown with the latest text
return;
}


FragmentManager fm = getSupportFragmentManager();
DialogFragment dialogFragment = (DialogFragment) fm.findFragmentByTag("foo");
if (dialogFragment != null)
{
dialogFragment.dismiss();
}


dialogFragment = GenericPromptSingleButtonDialogFragment.newInstance("title", "message", "button");
dialogFragment.show(fm, "foo");


sHandler.postDelayed(test, 5000);
}
};

将您的对话框片段对象设置为全局的,并在 onPause ()方法中调用 distisAllowingStateloss ()

@Override
protected void onPause() {
super.onPause();


if (dialogFragment != null) {
dialogFragment.dismissAllowingStateLoss();
}
}

不要忘记在片段中赋值,并在单击按钮或任何地方调用 show ()。

用这个密码

FragmentTransaction ft = fm.beginTransaction();
ft.add(yourFragment, "fragment_tag");
ft.commitAllowingStateLoss();

而不是

yourFragment.show(fm, "fragment_tag");

通过使用反射,我已经找到了这个问题的一个优雅的解决方案。 上述所有解决方案的问题在于字段 解散MShowByMe不改变它们的状态。

只需覆盖方法“显示”在您自己的自定义底部工作表对话框片段,如下面的示例(Kotlin)

override fun show(manager: FragmentManager, tag: String?) {
val mDismissedField = DialogFragment::class.java.getDeclaredField("mDismissed")
mDismissedField.isAccessible = true
mDismissedField.setBoolean(this, false)


val mShownByMeField = DialogFragment::class.java.getDeclaredField("mShownByMe")
mShownByMeField.isAccessible = true
mShownByMeField.setBoolean(this, true)


manager.beginTransaction()
.add(this, tag)
.commitAllowingStateLoss()
}

使用 Activity-KTX 的新生命周期范围与下面的代码示例一样简单:

lifecycleScope.launchWhenResumed {
showErrorDialog(...)
}

这个方法可以在 onStop ()之后直接调用,并在返回 onResume ()时成功显示对话框。

如果覆盖 show ()函数,请不要这样做:

override fun show(manager: FragmentManager, tag: String?) {
// mDismissed = false; is removed -> lead to wrong state
// mShownByMe = true; is removed -> lead to wrong state
val ft = manager.beginTransaction()
ft.add(this, tag)
ft.commitAllowingStateLoss()
}

这可能会导致错误的对话状态

只要做:

override fun show(manager: FragmentManager, tag: String?) {
try {
super.show(manager, tag)
} catch (e: Exception) {
val ft = manager.beginTransaction()
ft.add(this, tag)
ft.commitAllowingStateLoss()
}
}