IllegalStateException при добавлении фрагмента из Backstack

Время от времени я получаю следующий отчет о сбое:

java.lang.IllegalStateException: 
  at androidx.fragment.app.FragmentManagerImpl.addFragment (FragmentManagerImpl.java:1916)
  at androidx.fragment.app.BackStackRecord.executePopOps (BackStackRecord.java:828)
  at androidx.fragment.app.FragmentManagerImpl.executeOps (FragmentManagerImpl.java:2622)
  at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether (FragmentManagerImpl.java:2411)
  at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute (FragmentManagerImpl.java:2366)
  at androidx.fragment.app.FragmentManagerImpl.execPendingActions (FragmentManagerImpl.java:2273)
  at androidx.fragment.app.FragmentManagerImpl$1.run (FragmentManagerImpl.java:733)
  at android.os.Handler.handleCallback (Handler.java:808)
  at android.os.Handler.dispatchMessage (Handler.java:101)
  at android.os.Looper.loop (Looper.java:166)
  at android.app.ActivityThread.main (ActivityThread.java:7529)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:245)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:921)

В строке 1916 FragmentManagerImpl я могу найти:

throw new IllegalStateException("Fragment already added: " + fragment);

Значит, там написано, что какой-то фрагмент уже добавлен. К сожалению, Google больше не показывает сообщение (какой фрагмент уже добавлен) в консоли Google Play. Насколько я понимаю, Stacktrace это исключение возникает при добавлении фрагмента из бэкстека, верно?

У меня есть один FrameLayout, в котором я добавляю/удаляю фрагменты. Я всегда добавляю их с помощью следующего кода:

public void addFragment(FragmentActivity activity, Fragment fragment) {
    FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
    String tag = fragment.getClass().getCanonicalName();
    Fragment prev = activity.getSupportFragmentManager().findFragmentByTag(tag);
    if (fragment.isAdded()) {
        return;
    }
    if (prev != null) {
        transaction.remove(prev);
    }
    transaction.replace(R.id.fragment_container, fragment, tag);
    transaction.addToBackStack(null);
    transaction.commit();

И я добавляю DialogFragments следующим методом:

public void openFragmentDialog(FragmentActivity activity, DialogFragment dialogFragment, String tag) {
    FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
    Fragment prev = activity.getSupportFragmentManager().findFragmentByTag(tag);
    if (prev != null) {
        transaction.remove(prev);
    }
    transaction.addToBackStack(null);
    dialogFragment.show(transaction, tag);
}

В какой ситуации может произойти исключение IllegalStateException? Я неправильно добавляю/заменяю фрагменты/диалоговые фрагменты? Я никогда не мог воспроизвести эту ошибку. Но я получаю отчеты с Android 4.4 - Android 9 и всех типов устройств, и я понятия не имею, где это могло произойти.

Может быть что-то с анимацией или медленными устройствами? Потому что это случается лишь изредка.


comment
Я все еще недостаточно хорошо понимаю фрагменты, чтобы ответить на ваш вопрос, но у меня были похожие вопросы, которые могут быть вам полезны: stackoverflow.com/ q/30627498/4107809 stackoverflow.com/q/30671291/4107809 Часть моей путаницы была с заменой и часть с несколькими копиями фрагментов.   -  person mattm    schedule 12.07.2019


Ответы (3)


Обратите внимание, что вы добавляете и удаляете два разных фрагмента с одним и тем же тегом — тогда тег также должен быть другим. Если фрагмент prev является экземпляром другого класса, почему вы используете имя класса fragment в качестве тега для его поиска?

Вы должны добавить оператор if, чтобы проверить, не являются ли иногда prev и fragment одним и тем же экземпляром (из-за одного и того же тега).

Если два фрагмента являются одним и тем же экземпляром, и вы все еще хотите удалить, а затем добавить один и тот же фрагмент, возможно, вам придется сделать это в двух разных транзакциях, чтобы предотвратить исключение "Fragment already added".

person jhavatar    schedule 13.07.2019
comment
Хорошо, и когда они являются одним и тем же экземпляром (что означает, что prev != null), я мог бы сделать что-то вроде if (prev.isVisible()) return; else // Create new transaction to remove prev first? Итак, в заключение я должен заменить transaction.remove(prev) на создание новой транзакции и удалить prev, верно? - person L3n95; 13.07.2019
comment
Я предполагаю, что в идеале prev и fragment не являются одним и тем же экземпляром - этого можно легко достичь, используя разные tag для каждого, например. String tag = fragment.getClass().getCanonicalName() + fragment.hashCode(); Вам просто нужно отслеживать уникальный тег в этом случае, чтобы потом удалить добавленный фрагмент. - person jhavatar; 13.07.2019
comment
В том же случае я имею в виду prev == fragment. Если это один и тот же экземпляр, то нужно ли вообще добавлять fragment, поскольку он уже добавлен/присутствует. - person jhavatar; 13.07.2019
comment
Если это один и тот же экземпляр, и вы все еще хотите удалить, а затем снова добавить fragment, я бы рекомендовал использовать 2 транзакции: activity.getSupportFragmentManager().beginTransaction().remove(fragment).commit(); activity.getSupportFragmentManager().beginTransaction().add(fragment).commit(); - person jhavatar; 13.07.2019
comment
@jhavatar, я думаю, транзакции асинхронны, поэтому вторая может начаться до того, как первая завершится. Итак, я использую commitNow() в первой транзакции (и replace вместо add во второй). Одинаково ли fragment в обеих транзакциях? - person CoolMind; 02.03.2020
comment
@CoolMind, возможно, вы правы насчет асинхронности - я просто предположил, что работа выполняется в одном потоке и выполняется в запланированном порядке. Что вы подразумеваете под тем, что фрагмент одинаков в обеих транзакциях?? Мой пример комментария (удалить, добавить) был для случая, когда fragment был одним и тем же экземпляром в обеих транзакциях. - person jhavatar; 03.03.2020
comment
@jhavatar, ну ладно, у меня тоже такой же экземпляр fragment. - person CoolMind; 03.03.2020

tag должны быть уникальными. Если возможно, замените его на null, иначе на что-то уникальное.

public void addFragment(FragmentActivity activity, Fragment fragment) {
    FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.fragment_container, fragment);
    transaction.addToBackStack(null);
    transaction.commit();
}
person Bertram Gilfoyle    schedule 12.07.2019
comment
Каждый фрагмент/диалоговый фрагмент имеет уникальный тег, если он добавлен/показан. - person L3n95; 12.07.2019
comment
Я говорю о двух экземплярах одного и того же фрагмента - person Bertram Gilfoyle; 12.07.2019
comment
Я надеялся, что никогда не случится, что у меня будет два экземпляра одного и того же фрагмента. Как может случиться, что есть два экземпляра? - person L3n95; 12.07.2019
comment
Я не могу быть уверен, как вы это реализовали. Если вы используете нижнюю навигацию, вы можете нажать фрагмент A, затем фрагмент B и снова фрагмент A, не нажимая кнопку «Назад» два раза. - person Bertram Gilfoyle; 12.07.2019
comment
Я не думаю, что это возможно в моем приложении. Вы видите только один фрагмент за раз и можете перемещаться вперед и назад с помощью кнопки и кнопки «Назад». Но я еще раз подумаю. - person L3n95; 12.07.2019
comment
Хорошо. Чтобы быть уверенным, вы можете заменить его на null. - person Bertram Gilfoyle; 12.07.2019
comment
Давайте продолжим обсуждение в чате. - person Bertram Gilfoyle; 12.07.2019

Вот что я сделал для проверки и предотвращения дублирования фрагментов:

MyFragment myFragment = (MyFragment) fragmentManager.findFragmentByTag(MY_FRAGMENT_TAG);

if(myFragment != null && myFragment.isVisible()){
    // Return early if the fragment already exists & is visible
    return;

}else if(myFragment == null){
    // Create a new instance of the fragment if none already exist
    myFragment = new MyFragment();
}

// Perform the fragment transaction
fragmentManager.beginTransaction()
    .replace(R.id.content_container, myFragment, MY_FRAGMENT_TAG)
    .addToBackStack(MY_FRAGMENT_TAG) // Pass in the tag if you want to add to the back stack
    .commit();

Для фрагментов диалога я просто создаю новый экземпляр и вызываю для него .show(). Диалог должен быть закрыт через взаимодействие с ним:

MyDialog myDialog = new MyDialog();

// I call getChildFragmentManager here b/c I was using this from within a fragment
// but you can get whichever Fragment Manager is appropriate
myDialog.show(getChildFragmentManager(), MY_DIALOG_TAG);
person Sammy T    schedule 18.07.2019