从onActivityResult中显示DialogFragment

84

我在我的一个碎片的onActivityResult中有以下代码:

onActivityResult(int requestCode, int resultCode, Intent data){
   //other code
   ProgressFragment progFragment = new ProgressFragment();  
   progFragment.show(getActivity().getSupportFragmentManager(), PROG_DIALOG_TAG);
   // other code
}

然而,我遇到了以下错误:

Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState   

有人知道发生了什么,或者我该如何解决这个问题吗?需要注意的是,我正在使用Android Support Package。

17个回答

76
如果您使用Android支持库,onResume方法不是与片段进行交互的正确位置。您应该在onResumeFragments方法中执行此操作,请参见onResume方法说明:http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html#onResume%28%29 因此,我认为正确的代码应该是:
private boolean mShowDialog = false;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
  super.onActivityResult(requestCode, resultCode, data);

  // remember that dialog should be shown
  mShowDialog = true;
}

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

  // play with fragments here
  if (mShowDialog) {
    mShowDialog = false;

    // Show only if is necessary, otherwise FragmentManager will take care
    if (getSupportFragmentManager().findFragmentByTag(PROG_DIALOG_TAG) == null) {
      new ProgressFragment().show(getSupportFragmentManager(), PROG_DIALOG_TAG);
    }
  }
}

13
+1,这是正确的答案。请注意Activity类中不存在onResumeFragments()方法。如果您正在使用基本的Activity,则应该使用onPostResume()方法。 - Alex Lockwood
3
在执行此解决方案之前,请阅读此文,以了解它为何是一种hack。在此问题的另一个解决方案的评论中隐藏着一个更简单的解决方案。 - twig
1
调用super.onActivityResult并不能防止IllegalStateException的发生,因此不是解决该问题的方法。 - demaksee
2
这个问题在谷歌上是第一个搜索结果,但我认为被接受的答案并不是最好的。相反,应该接受这个答案:https://dev59.com/0mkw5IYBdhLWcg3wJHCF#30429551 - JHH

28

@Natix留下的评论可能是一个简短的一句话,有些人可能已经删除了。

解决这个问题最简单的方法是在运行自己的代码之前调用super.onActivityResult()。无论您是否使用支持库,此方法都有效,并保持Activity的行为一致性。

还有:

  • 无需使用线程、处理程序或睡眠来添加虚假延迟。
  • 无需提交以允许状态丢失或子类化以覆盖show()。这不是一个错误,而是一个警告。不要丢弃状态数据。(另一个,奖励示例)
  • 无需在活动中跟踪对话框片段(内存泄漏!)
  • 绝对不要通过手动调用onStart()来搞乱活动生命周期。

我读得越多,看到的疯狂黑客就越多。

如果你仍然遇到问题,那么Alex Lockwood的这个应该是需要检查的。

  • 不需要为 onSaveInstanceState() 编写任何代码(另一个)

如果你使用了继承,那会发生什么呢?如果你想在调用super之前先运行自己的代码,那么调用super.onActivityResult()可能会有问题,因为父类中的onActivityResult方法可能已经包含了其自身的代码,所以请小心处理。 - Ricard
我在我的代码之前添加了 super.onActivityResult(requestCode, resultCode, data),这解决了我的问题。但是当添加继承或覆盖默认的onActivityResult时,我们应该手动处理onStart/onResume。 - mochadwi

27

编辑: 这不是一个错误,而更像是片段框架中的缺陷。对于这个问题更好的答案是由 @Arcao 提供的。

---- 原始帖子 ----

实际上这是支持包中的 已知错误(编辑:实际上不是一个错误,请参见 @alex-lockwood 的评论)。在错误报告的评论中发布了一个解决方法,即修改 DialogFragment 的源代码,如下所示:

public int show(FragmentTransaction transaction, String tag) {
    return show(transaction, tag, false);
}


public int show(FragmentTransaction transaction, String tag, boolean allowStateLoss) {
    transaction.add(this, tag);
    mRemoved = false;
    mBackStackId = allowStateLoss ? transaction.commitAllowingStateLoss() : transaction.commit();
    return mBackStackId;
}

注意,这只是一个巨大的hack。我实际上是制作了自己的对话框片段,可以从原片段中注册。当另一个对话框片段执行操作(例如被解除显示)时,它会告诉任何监听器它正在消失。我是这样做的:

public static class PlayerPasswordFragment extends DialogFragment{

 Player toJoin;
 EditText passwordEdit;
 Button okButton;
 PlayerListFragment playerListFragment = null;

 public void onCreate(Bundle icicle){
   super.onCreate(icicle);
   toJoin = Player.unbundle(getArguments());
   Log.d(TAG, "Player id in PasswordFragment: " + toJoin.getId());
 }

 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle){
     View v = inflater.inflate(R.layout.player_password, container, false);
     passwordEdit = (EditText)v.findViewById(R.id.player_password_edit);
     okButton = (Button)v.findViewById(R.id.ok_button);
     okButton.setOnClickListener(new View.OnClickListener(){
       public void onClick(View v){
         passwordEntered();
       }
     });
     getDialog().setTitle(R.string.password_required);
     return v;
 }

 public void passwordEntered(){
   //TODO handle if they didn't type anything in
   playerListFragment.joinPlayer(toJoin, passwordEdit.getText().toString());
   dismiss();
 }

 public void registerPasswordEnteredListener(PlayerListFragment playerListFragment){
   this.playerListFragment = playerListFragment;
 }

 public void unregisterPasswordEnteredListener(){
   this.playerListFragment = null;
 }
}

现在我有一种方法可以在发生事情时通知PlayerListFragment。请注意,适当调用unregisterPasswordEnteredListener非常重要(在上述情况下,每当PlayerListFragment“消失”时),否则该对话框片段可能会尝试在已不存在的侦听器上调用函数。


3
不需要复制源代码的解决方案……只需覆盖 show() 方法,然后捕获 IllegalStateException 异常即可。 - Jeffrey Blattman
1
如何修改DialogFragment的源代码?或者您能否发布您在帖子末尾提到的解决方案? - Piotr Ślesarew
1
@PeterSlesarew 我发布了我的(相当具体的)解决方案。 - Kurtis Nusbaum
9
啊,这不是一个Bug!Android框架故意抛出异常,因为在onActivityResult()方法中执行Fragment事务是不安全的!尝试使用此解决方案:https://dev59.com/9WQo5IYBdhLWcg3wKstr#18345899 - Alex Lockwood
2
@AlexLockwood 当这个问题被问到时,文档没有警告这一点。此外,虽然您的解决方案现在看起来不错,但在2012年4月份它并不起作用。onPostResumeonResumeFragments都是相对较新的支持库添加。 - hrnt
显示剩余2条评论

14

我认为这是一个 Android 的 bug。基本上,在 activity/fragment 生命周期的错误时刻(在 onStart() 之前),Android 将调用 onActivityResult。

该 bug 在 https://issuetracker.google.com/issues/36929762 上有报告。

我解决了这个问题,基本上是将 Intent 存储为稍后在 onResume() 中处理的参数。

[编辑] 现在有更好的解决方案可用于此问题,而这些解决方案在 2012 年并不存在。请参见其他答案。


8
实际上那并不是一个真正的 bug。正如评论中指出的那样,它清楚地说明了 onActivityResult()onResume() 之前被调用。 - Kurtis Nusbaum
2
你读了最后一个关于 bug 的评论吗?这个 bug 是指 onActivityResult() 在 onStart() 之前被调用,而不是在 onResume() 之前被调用。 - hrnt
啊,没错。我漏掉了那个。虽然我仍然认为另一个错误报告对我的问题更相关。 - Kurtis Nusbaum
当调用onActivityResult时,它是明确定义的。因此,即使在某些情况下可能看起来不合适,它也不能成为一个错误。 - sstn
1
@sstn,你能详细说明一下吗?当onActivityResult被调用时是很明确的(在onResume之前立即调用)。Android不会在onResume之前立即调用onActivityResult。因此,这是一个bug。 - hrnt

11

编辑:另一种选择,也可能是迄今为止最好的选择(或者至少是支持库所期望的……)

如果您正在使用 Android 支持库中的 DialogFragments,请使用 FragmentActivity 的子类。请尝试以下方法:

onActivityResult(int requestCode, int resultCode, Intent data) {

   super.onActivityResult(requestCode, resultCode, intent);
   //other code

   ProgressFragment progFragment = new ProgressFragment();  
   progFragment.show(getActivity().getSupportFragmentManager(), PROG_DIALOG_TAG);

   // other code
}

我查看了FragmentActivity的源代码,似乎它调用了一个内部的fragment管理器来恢复fragment而不会失去状态。


我找到了一种这里没有列出的解决方案。我创建了一个Handler,在Handler中启动对话框片段。因此,稍微编辑您的代码:


返回:

我找到了一种这里没有列出的解决方案。我创建了一个Handler,在Handler中启动对话框片段。因此,稍微编辑您的代码:

onActivityResult(int requestCode, int resultCode, Intent data) {

   //other code

   final FragmentManager manager = getActivity().getSupportFragmentManager();
   Handler handler = new Handler();
   handler.post(new Runnable() {
       public void run() {
           ProgressFragment progFragment = new ProgressFragment();  
           progFragment.show(manager, PROG_DIALOG_TAG);
       }
   }); 

  // other code
}

我个人认为这样更干净、不那么hacky。


5
使用Handler解决这个问题只会添加延迟,因此使问题发生的可能性更小。但它不能保证问题会消失!这有点像使用Thread#sleep()解决竞态条件。 - Alex Lockwood
27
调用 super.onActivityResult() 是目前最简单有效的解决方案,这很可能会成为被接受的答案!我偶然发现了缺失的 super 调用,并惊喜地发现加上它就可以运行了。这使我可以移除该页面中提到的一种旧的 hack(将对话框保存在临时变量中,在 onResume() 中显示)。 - Natix
好的解决方案。唯一的问题是 onActivityResult() 没有返回任何值来指示片段是否处理了结果。 - Michael
调用super.onActivityResult()不能解决我的项目中的IllegalStateException崩溃问题。 - demaksee

9

有两个DialogFragment的show()方法 - show(FragmentManager manager, String tag)show(FragmentTransaction transaction, String tag)

如果你想使用FragmentManager版本的方法(就像原始问题中一样),一个简单的解决方案是重写这个方法并使用commitAllowingStateLoss:

public class MyDialogFragment extends DialogFragment {

  @Override 
  public void show(FragmentManager manager, String tag) {
      FragmentTransaction ft = manager.beginTransaction();
      ft.add(this, tag);
      ft.commitAllowingStateLoss();
  }

}

覆盖 show(FragmentTransaction,String)的方式并不容易,因为它还应修改原始DialogFragment代码中的某些内部变量,因此我不建议这样做 - 如果您想使用该方法,请尝试接受的答案中的建议(或Jeffrey Blattman的评论)。使用commitAllowingStateLoss存在一定风险-文档指出,“类似于commit(),但允许在保存活动状态后执行提交。这很危险,因为如果将来需要从其状态恢复活动,则提交可能会丢失,因此只应用于UI状态意外更改对用户没有影响的情况。”

4

在调用活动的onSaveInstanceState()方法后,您无法显示对话框。显然,在onActivityResult()之前调用了onSaveInstanceState()。因此,您应该在回调方法OnResumeFragment()中显示对话框,而不需要覆盖DialogFragment的show()方法。希望这可以帮助您。


3

onActivityResult() 在 onResume() 之前执行。您需要在 onResume() 或更晚的时间处理 UI。

使用布尔值或其他需要的方式,在这两种方法之间进行结果通信。

... 就是这样。简单明了。


3
我想出了第三种解决方案,部分基于hmt的解决方案。基本上,创建一个DialogFragment的ArrayList,在onResume()中显示它们。
ArrayList<DialogFragment> dialogList=new ArrayList<DialogFragment>();

//Some function, like onActivityResults
{
    DialogFragment dialog=new DialogFragment();
    dialogList.add(dialog);
}


protected void onResume()
{
    super.onResume();
    while (!dialogList.isEmpty())
        dialogList.remove(0).show(getSupportFragmentManager(),"someDialog");
}

2

这是因为当调用#onActivityResult()时,父活动已经调用了#onSaveInstanceState()。

我会使用一个Runnable来在#onActivityResult()中“保存”操作(显示对话框),以便稍后在活动准备就绪时使用它。

通过这种方法,我们确保我们想要的操作始终有效。

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == YOUR_REQUEST_CODE) {
        mRunnable = new Runnable() {
            @Override
            public void run() {
                showDialog();
            }
        };
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

@Override
public void onStart() {
    super.onStart();
    if (mRunnable != null) {
        mRunnable.run();
        mRunnable = null;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接