Backstack management: перезапуск должен быть создан только на этапе инициализации владельца.

Я использую нижнюю панель навигации в MainActivity для обработки некоторых фрагментов. Это код, используемый для переключения между ними:

private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
    if (item.isChecked &&
        supportFragmentManager.findFragmentById(R.id.act_main_fragment_container) != null
    )
        return@OnNavigationItemSelectedListener false
    val fragment =
        when (item.itemId) {
            R.id.navigation_home      -> fragments[0]
            R.id.navigation_bookings  -> fragments[1]
            R.id.navigation_messages  -> fragments[2]
            R.id.navigation_dashboard -> fragments[3]
            R.id.navigation_profile   -> fragments[4]
            else                      -> fragments[0]
        }
    this replaceWithNoBackStack fragment
    return@OnNavigationItemSelectedListener true
}

метод replaceWithNoBackstack - это всего лишь сокращение для этого:

supportFragmentManager
    ?.beginTransaction()
    ?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
    ?.replace(containerId, fragment)
    ?.commit()

Проблема в том, что когда я быстрее переключаюсь между ними, мое приложение вылетает со следующим исключением:

java.lang.IllegalStateException: перезапуск должен быть создан только на этапе инициализации владельца в androidx.savedstate.SavedStateRegistryController.performRestore (SavedStateRegistryController.java:59) в androidx.fragment.app.Fragment.performCreate (Fragment.java:25. fragment.app.FragmentManagerImpl.moveToState (FragmentManagerImpl.java:837) на androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState (FragmentManagerImpl.java:1237) на androidx.fragment.app.Fragment.ManagerManager1. .fragment.app.BackStackRecord.executeOps (BackStackRecord.java:439) в androidx.fragment.app.FragmentManagerImpl.executeOps (FragmentManagerImpl.java:2075) в androidx.fragment.app.FragmentManagerImplager.executeOpsMava:Together18 androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute (FragmentManagerIm pl.java:1820) на androidx.fragment.app.FragmentManagerImpl.execPendingActions (FragmentManagerImpl.java:1726) на androidx.fragment.app.FragmentManagerImpl $ 2.run (FragmentManagerImpl.java:150) на android.os.Handler.handle Handler.java:789) на android.os.Handler.dispatchMessage (Handler.java:98) на android.os.Looper.loop (Looper.java:164) на android.app.ActivityThread.main (ActivityThread.java:6709 ) в java.lang.reflect.Method.invoke (собственный метод) в com.android.internal.os.Zygote $ MethodAndArgsCaller.run (Zygote.java:240) в com.android.internal.os.ZygoteInit.main (ZygoteInit .java: 769) Я много искал и не нашел ответа.

Я также получаю эту ошибку, если выполняю вызов API, помещаю приложение в фоновый режим, жду ответа, и когда я возвращаюсь в приложение, приложение вылетает из-за того, что я пытаюсь немедленно отобразить фрагмент диалогового окна (причина Я думаю, что это происходит из-за того, что транзакция воссоздания фрагмента при возврате из фона все еще выполняется во время отображения фрагмента диалога). Я решил это хакерским способом, установив задержку в 500 мс для диалога, потому что я не мог найти других решений.

Пожалуйста, спросите, нужны ли вам более подробные сведения по этому поводу. Заранее спасибо!

ВОЗМОЖНЫЕ ТЕМПЕРАТУРНЫЕ РЕШЕНИЯ

ИЗМЕНИТЬ. Я решил эту проблему, понизив зависимость совместимости приложений до androidx.appcompat:appcompat:1.0.2, но это временное решение, так как мне придется обновить его в будущем. Надеюсь, кто-нибудь во всем разберется.

ИЗМЕНИТЬ 2. Я решил проблему, удалив setTransition () из транзакций фрагментов. По крайней мере, я знаю причину, по которой приложения для Android не имеют хороших переходов в целом

РЕДАКТИРОВАТЬ 3. Возможно, лучшее решение, чтобы избежать этой проблемы, а также обеспечить бесперебойную работу, - это просто использовать ViewPager для обработки нижней панели навигации.


person Gabi    schedule 11.06.2019    source источник
comment
Что это за массив fragments[]? Я подозреваю, что проблема в том, что вы повторно используете фрагменты, а не каждый раз создаете новые.   -  person Ben P.    schedule 12.06.2019
comment
Мне нужно использовать их повторно, поэтому я храню их в таком виде   -  person Gabi    schedule 12.06.2019
comment
Вы нашли способ исправить это без перехода на 1.0.2?   -  person Roudi    schedule 26.06.2019
comment
К сожалению нет. Придется ждать новых релизов   -  person Gabi    schedule 26.06.2019
comment
Вероятно, это потому, что вы пытаетесь использовать replace для добавления фрагмента, который в настоящее время является isRemoving.   -  person EpicPandaForce    schedule 22.10.2019
comment
Мне также пришлось вернуться к androidx.appcompat: appcompat: 1.0.2, чтобы избежать этого исключения.   -  person MikeOscarEcho    schedule 29.10.2019


Ответы (11)


Если вы используете androidx.core: core-ktx: 1.0.2, попробуйте перейти на 1.0.1

Если вы используете жизненный цикл (или rxFragment) и androidx_appcompat: alpha05, попробуйте изменить версию.
ex) appcompat: 1.1.0-beta01 или 1.0.2

Я думаю, что это проявляется как ошибка при сохранении состояния при повторном использовании целевого фрагмента (onPause-onResume).

person 진홍빛    schedule 12.06.2019
comment
Не могли бы вы объяснить, почему это может решить проблему? - person Souradeep Nanda; 12.06.2019
comment
Я уже использую androidx.appcompat:appcompat:1.1.0-beta01 в своем приложении - person Gabi; 12.06.2019

поскольку версия 1.0.0 не проверяет состояние, поэтому исключение не будет генерироваться, но версия 1.1.0 изменяет исходный код , поэтому генерирует исключение. .

это исходный код Fragment версии 1.1.0, он вызовет метод performRestore

    void performCreate(Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mState = CREATED;
        mCalled = false;
        mSavedStateRegistryController.performRestore(savedInstanceState);
        onCreate(savedInstanceState);
        mIsCreated = true;
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onCreate()");
        }
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    }

/**
the exception
**/
public void performRestore(@Nullable Bundle savedState) {
        Lifecycle lifecycle = mOwner.getLifecycle();
        if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
            throw new IllegalStateException("Restarter must be created only during "
                    + "owner's initialization stage");
        }
        lifecycle.addObserver(new Recreator(mOwner));
        mRegistry.performRestore(lifecycle, savedState);
    }

это исходный код версии 1.0.0 , не вызывает performRestore, поэтому исключение не будет

void performCreate(Bundle savedInstanceState) {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
    }
    mState = CREATED;
    mCalled = false;
    onCreate(savedInstanceState);
    mIsCreated = true;
    if (!mCalled) {
        throw new SuperNotCalledException("Fragment " + this
                + " did not call through to super.onCreate()");
    }
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}

Есть два разных решения, которые могут справиться с этим:
Первое решение - разделить транзакцию。
Поскольку мы всегда используем replace или объединяем remove и add в одна транзакция. Мы можем разделить транзакцию на две транзакции следующим образом:

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        //commit immediately
            ft.remove(prev).commitAllowingStateLoss();
        }
        FragmentTransaction addTransaction = manager.beginTransaction();
        addTransaction.addToBackStack(null);
        addTransaction.add(layoutId, fragment,
                tag).commitAllowingStateLoss();

потому что эти две транзакции будут двумя разными Сообщениями, которые будут обрабатываться Обработчиком.
Второе решение - проверить состояние заранее. мы можем следить за исходным кодом , заранее проверить состояние

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        if (prev.getLifecycle().getCurrentState() != Lifecycle.State.INITIALIZED) {
            return;
        }
            ft.remove(prev);
        }

Я рекомендую первый способ, потому что второй способ - это следование исходному коду, если исходный код изменит код, он будет недействительным。

person Drown Coder    schedule 27.06.2019
comment
Тогда как лучше всего с этим справиться? Я понял, что это происходит, когда вы начинаете транзакцию, в то время как другая транзакция уже выполняется, поэтому, когда пользователь открывает фрагмент, а затем сразу же возвращается, он вылетает. - person Roudi; 11.07.2019
comment
@PampaZiya Я отредактировал ответ, добавив решение - person Drown Coder; 11.07.2019
comment
а что, если вы хотите анимировать удаление и добавление? - person Roudi; 11.07.2019

У меня такая же проблема.

val fragment = Account.activityAfterLogin
        val ft = activity?.getSupportFragmentManager()?.beginTransaction()
        //error
        ft?.setCustomAnimations(android.R.anim.slide_in_left,android.R.anim.slide_out_right)0
        ft?.replace(R.id.framelayout_account,fragment)
        ft?.commit()

Смена версии библиотеки не помогла. Я решил это, добавив строку ft?.AddToBackStack(null) после метода ft?.setCustomAnimations (), и все. Анимация работает и вылетов нет.

person Mako Storm    schedule 04.09.2019
comment
У меня та же проблема, что приложение вылетает, когда я пытаюсь использовать анимацию, есть ли альтернатива ft? .AddToBackStack (null) ?? - person vinay kumar; 09.01.2020
comment
Это был единственный способ заставить его работать при использовании setCustomAnimations()! В моем приложении происходил сбой при запуске новой FragmentTransaction до того, как предыдущая анимация не была завершена. Потратил на это часы, спасибо. Все еще странно, как и почему это решает проблему. - person BenjyTec; 18.02.2020
comment
На самом деле это тоже мое единственное решение. Но это добавило бы запись в историю для добавленных фрагментов. Мне действительно нужно переопределить onBackPress и завершить действие вручную? - person Hatzen; 10.08.2020

Я изменил реализацию на api для androidx.appcompat: appcompat: 1.0.2, и это сработало для меня

person Vlad Yatsenko    schedule 21.06.2019

Если это может помочь, я столкнулся с той же проблемой с BottomNavigationView и setCustomAnimations, в основном за счет быстрого переключения между фрагментами, вы можете в конечном итоге запустить FragmentTransaction, пока предыдущий не был завершен, а затем он вылетает.

Чтобы этого избежать, я отключаю панель навигации до завершения перехода. Итак, я создал метод для включения / отключения элементов BottomNavigationView (отключение самого BottomNavigationView не отключает меню, или я не нашел способ), а затем снова включаю их после завершения перехода.

Чтобы отключить элементы, я вызываю следующий метод прямо перед запуском FragmentTransition:

public void toggleNavigationBarItems(boolean enabled) {
   Menu navMenu = navigationView.getMenu();
   for (int i = 0; i < navMenu.size(); ++i) {
       navMenu.getItem(i).setEnabled(enabled);
   }
}

Чтобы снова включить их, я создал абстрактный класс Fragment для фрагментов, загруженных из BottomNavigationView. В этом классе я переопределяю onCreateAnimator (если вы используете View Animation, вы должны переопределить onCreateAnimation) и повторно включаю их onAnimationEnd.

@Nullable
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
    if(enter){ // check the note below
        Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                myActivity.toggleNavigationBarItems(true)
            }
        });
        return animator;
    }
    return super.onCreateAnimator(transit, enter, nextAnim);
}

Примечание: поскольку мои анимации входа и выхода имеют одинаковую продолжительность, мне не нужно синхронизировать их, поскольку анимация входа начинается после анимации выхода. Вот почему if (enter) достаточно.

person Jonathan Kely Kosi    schedule 29.10.2019

Я исправил эту проблему, добавив 'synchronized' в метод добавления фрагмента.

до:

public void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}

после:

public synchronized void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}
person Mahdi Zareei    schedule 14.05.2020

Кажется, эта ошибка решена с помощью androidx.appcompat: appcomat: 1.1.0-rc01 и androidx.fragment: fragment: 1.1.0-rc03.

https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-rc03

person Geoffrey Powell    schedule 25.07.2019
comment
Я попытался обновить оба, но ничего не вышло. Если вы нанесете ответный удар во время анимации для нового фрагмента (в середине транзакции), у меня просто произойдет сбой. Я не использую pop, я просто инициирую новую транзакцию замены, что приводит к той же ошибке - person Roudi; 26.07.2019
comment
Да, правильно, это все еще происходит в некоторых случаях в rc03 :( - person Geoffrey Powell; 31.07.2019
comment
Да, это не было исправлено даже в версии 1.2.0-alpha01. Интересно, как лучше всего подойти к этому? - person Roudi; 08.08.2019
comment
@PampaZiya Если у вас есть способ воспроизвести проблему, можете ли вы опубликовать образец проекта в системе отслеживания проблем? Некоторые из нас видят это только в трекере Google Play, и для этого требуется точное время, которое невозможно воспроизвести. - person Dmitry; 24.12.2019
comment
@Dmitry Вы можете воспроизвести, добавив анимацию между фрагментами и нажав кнопку «Назад» во время анимации фрагмента. - person Roudi; 24.12.2019

У меня такая проблема при использовании setCustomAnimations. удалив setCustomAnimations, я решил мою проблему. также у меня нет проблем, когда я создаю новый экземпляр фрагмента перед его показом даже с помощью setCustomAnimation.

РЕДАКТИРОВАТЬ: другой способ - добавить фрагмент в стопку.

person Hadi Ahmadi    schedule 29.09.2019
comment
Я думаю, что добавление в backstack решит проблему. Не удалось удалить анимацию, так как она требовалась мне. Спасибо. - person Shashank Degloorkar; 19.10.2020

Я смог исправить это (надеюсь ????), используя commitNow() вместо commit() для всех транзакций нижнего навигационного фрагмента. Мне больше нравится этот подход, поскольку он позволяет по-прежнему использовать настраиваемые переходы между фрагментами.

Примечание. Это решение применяется только в том случае, если вы не хотите, чтобы ваши нижние навигационные транзакции добавлялись в бэкстэк (чего вы в любом случае делать не должны).

person simekadam    schedule 25.11.2019

Ничего не работало, кроме решения Drown Coder, но оно все равно не было идеальным, поскольку добавляло транзакции в backstack. Таким образом, если вы нажмете все кнопки в нижней части навигации, у вас будет как минимум по 1 фрагменту каждого фрагмента в стеке. Я немного улучшил это решение, поэтому вы не используете .replace(), приводящий к сбою приложения с анимацией действий.

Вот код:

if (getChildFragmentManager().getBackStackEntryCount() > 0) {
    getChildFragmentManager().popBackStack();
}

FragmentTransaction addTransaction = getChildFragmentManager().beginTransaction();
addTransaction.setCustomAnimations(R.animator.fragment_fade_in, R.animator.fragment_fade_out);
addTransaction.addToBackStack(null);
addTransaction.add(R.id.frame, fragment, fragment.getClass().getName()).commitAllowingStateLoss();
person Dmitriy Pavlukhin    schedule 04.12.2019

Я нашел другой способ создания этого дела.

CASE-1

  1. Надуть фрагмент в рамке-макете на мероприятии
  2. запустить запрос API (не использовать ответ API, когда приложение находится на переднем плане)
  3. Держите приложение в фоновом режиме
  4. Использовать запрос API (предположим, вы хотите добавить еще один фрагмент в ответ API)
  5. Надуйте другой фрагмент с помощью метода .replace () в том же макете фрейма.
  6. Вы сможете создать Crash

CASE-2

  1. Надуть фрагмент в рамке-макете на мероприятии
  2. Начать запрос API
  3. Используйте api на переднем плане (предположим, вы хотите добавить еще один фрагмент в ответ api, используя метод .replace () менеджера фрагментов)
  4. Поместите приложение в фоновый режим
  5. Воссоздайте свое приложение (вы можете сделать это, используя Не сохранять действия, изменение разрешений, изменение языка системы)
  6. Вернитесь к своему приложению
  7. Ваша деятельность начнет воссоздавать
  8. Действие автоматически воссоздает свой уже раздутый фрагмент, предположим, что он (точка-1)
  9. Убедитесь, что API снова запрашивается в случае повторного создания, после пункта 8
  10. Использовать ответ API и раздуть другой фрагмент с помощью метода .replace ()
  11. Вы сможете создать сбой (так как в этом случае уже выполняется переход в точке-8, а вы добавляете еще один фрагмент в точку-10)
person Kumar Purushottam    schedule 03.12.2020