IllegalStateException: не может выполнить это действие после onSaveInstanceState с ViewPager - PullRequest
445 голосов
/ 28 сентября 2011

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

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)

Очевидно, это как-то связано с FragmentManager, которым я не пользуюсь. В трассировке стека нет ни одного из моих собственных классов, поэтому я понятия не имею, где происходит это исключение и как его предотвратить.

Для записи: у меня есть вкладка, и на каждой вкладке есть группа действий, переключающаяся между действиями.

Ответы [ 32 ]

683 голосов
/ 21 апреля 2012

Пожалуйста, проверьте мой ответ здесь .В основном мне просто нужно было:

@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

Не звонить super() по методу saveInstanceState.Это все испортило ...

Это известная ошибка в пакете поддержки.

Если вам нужно сохранить экземпляр и добавить что-то к вашему outState Bundle, вы можете использовать следующее:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
    super.onSaveInstanceState(outState);
}

В конце концов, правильное решение было (как видно изкомментарии) для использования:

transaction.commitAllowingStateLoss();

при добавлении или выполнении FragmentTransaction, вызвавшем Exception.

116 голосов
/ 09 января 2015

Есть много связанных проблем с похожим сообщением об ошибке.Проверьте вторую строку этой конкретной трассировки стека.Это исключение конкретно относится к вызову FragmentManagerImpl.popBackStackImmediate.

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

  • Удаление звонка на super.onSaveInstanceState не поможет.
  • Создание фрагмента с помощью commitAllowingStateLoss не поможет.

Вот как я наблюдал проблему:

  • Есть форма с кнопкой отправки.
  • При нажатии кнопки создается диалоговое окно и запускается асинхронный процесс.
  • Пользователь нажимает клавишу home до завершения процесса - вызывается onSaveInstanceState.
  • Процесс завершается, выполняется обратный вызов и предпринимается попытка popBackStackImmediate.
  • IllegalStateException.

Вот что я сделал, чтобы решить эту проблему:

Поскольку невозможно избежать IllegalStateException в обратном вызове, поймайте и проигнорируйте его.

try {
    activity.getSupportFragmentManager().popBackStackImmediate(name);
} catch (IllegalStateException ignored) {
    // There's no way to avoid getting this if saveInstanceState has already been called.
}

Этого достаточно, чтобы остановить сбой приложения.Но теперь пользователь восстановит приложение и увидит, что кнопка, которую, как они думали, нажали, вообще не была нажата (они думают).Фрагмент формы все еще показывает!

Чтобы исправить это, когда диалоговое окно создано, создайте некоторое состояние, чтобы указать, что процесс запущен.

progressDialog.show(fragmentManager, TAG);
submitPressed = true;

И сохраните это состояние в комплекте.

@Override
public void onSaveInstanceState(Bundle outState) {
    ...
    outState.putBoolean(SUBMIT_PRESSED, submitPressed);
}

Не забудьте снова загрузить его в onViewCreated

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

@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);
        submitPressed = false;
    }
}
53 голосов
/ 04 марта 2015

Проверьте, если действие isFinishing(), прежде чем показывать фрагмент, и обратите внимание на commitAllowingStateLoss().

Пример:

if(!isFinishing()) {
FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction ft = fm.beginTransaction();
            DummyFragment dummyFragment = DummyFragment.newInstance();
            ft.add(R.id.dummy_fragment_layout, dummyFragment);
            ft.commitAllowingStateLoss();
}
21 голосов
/ 04 октября 2017

Это октябрь 2017 года, и Google создает библиотеку поддержки Android с новыми компонентами, называемыми компонентом жизненного цикла. Это дает новую идею для этой проблемы «Не удается выполнить это действие после onSaveInstanceState».

Короче говоря:

  • Используйте компонент жизненного цикла, чтобы определить, подходит ли вам время для всплытия вашего фрагмента.

Более длинная версия с объяснением:

  • почему эта проблема выходит?

    Это потому, что вы пытаетесь использовать FragmentManager из вашей деятельности (которая будет содержать ваш фрагмент, я полагаю?) Для совершения транзакции для вашего фрагмента. Обычно это выглядит так, как будто вы пытаетесь выполнить какую-то транзакцию для предстоящего фрагмента, в то время как действия хоста уже вызывают метод savedInstanceState (пользователь может коснуться кнопки home, поэтому операция вызывает onStop(), в моем случае это причина)

    Обычно этой проблемы не должно быть - мы всегда пытаемся загрузить фрагмент в действие в самом начале, например, метод onCreate() является идеальным местом для этого. Но иногда это действительно происходит , особенно когда вы не можете решить, какой фрагмент вы будете загружать для этого действия, или вы пытаетесь загрузить фрагмент из блока AsyncTask (или что-то займет немного времени) , Время до того, как транзакция фрагмента действительно произойдет, но после метода действия onCreate() пользователь может сделать что угодно. Если пользователь нажмет кнопку «Домой», которая активирует метод действия onSavedInstanceState(), произойдет сбой can not perform this action.

    Если кому-то захочется глубже в этом вопросе, я предлагаю им взглянуть на этот блог post . Он смотрит глубоко в слой исходного кода и многое объясняет по этому поводу. Кроме того, это дает причину, по которой вам не следует использовать метод commitAllowingStateLoss() для обхода этого сбоя (поверьте, он не предлагает ничего хорошего для вашего кода)

  • Как это исправить?

    • Должен ли я использовать commitAllowingStateLoss() метод для загрузки фрагмента? Нет, вы не должны ;

    • Должен ли я переопределить метод onSaveInstanceState, игнорировать метод super внутри него? Нет, вы не должны ;

    • Должен ли я использовать магическую isFinishing внутреннюю активность, чтобы проверить, находится ли активность хоста в нужный момент для транзакции фрагмента? Да, это выглядит как правильный способ сделать.

  • Посмотрите, на что способен компонент Lifecycle .

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

    Я немного кодирую для своего собственного проекта, вот что я делаю, используя LifeCycle. Я кодирую в Котлине.

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

Как я покажу выше. Я проверю состояние жизненного цикла активности хоста. С компонентом Lifecycle в библиотеке поддержки это может быть более конкретным. Код lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED) означает, что если текущее состояние не меньше onResume, не позднее его? Это гарантирует, что мой метод не будет выполняться во время какого-либо другого состояния жизни (например, onStop).

  • Это все сделано?

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

    Так что здесь я хотел бы, чтобы могло быть что-то более приятное: приложение не будет аварийно завершать работу, если оно придет в рабочее состояние позднее, чем onResume, метод транзакции - это информация о состоянии жизни; кроме того, после того, как пользователь вернется в наше приложение, действие будет пытаться продолжить выполнение этого фрагмента транзакции.

    Я добавлю еще кое-что к этому методу:

class FragmentDispatcher(_host: FragmentActivity) : LifecycleObserver {
    private val hostActivity: FragmentActivity? = _host
    private val lifeCycle: Lifecycle? = _host.lifecycle
    private val profilePendingList = mutableListOf<BaseFragment>()

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

Я поддерживаю список внутри этого класса dispatcher, чтобы сохранить эти фрагменты, у которых нет шансов завершить транзакцию.действие.И когда пользователь возвращается с домашнего экрана и обнаруживает, что фрагмент еще не запущен, он переходит к методу resume() под аннотацией @OnLifecycleEvent(Lifecycle.Event.ON_RESUME).Теперь я думаю, что это должно работать так, как я ожидал.

21 голосов
/ 16 октября 2012

Вот другое решение этой проблемы.

Используя закрытую переменную-член, вы можете установить возвращаемые данные как намерение, которое затем может быть обработано после super.onResume ();

Вот так:

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;
    }
}
18 голосов
/ 08 апреля 2014

Короткое и рабочее решение:

Выполните простые шаги

Steps

Шаг 1: Переопределить состояние onSaveInstanceState в соответствующем фрагменте. И удалите супер метод из него.

 @Override
public void onSaveInstanceState( Bundle outState ) {

}  

Шаг 2. Использование fragmentTransaction.commitAllowingStateLoss( );

вместо fragmentTransaction.commit( ); во время операций с фрагментами.

12 голосов
/ 13 ноября 2015

ВНИМАНИЕ , использование transaction.commitAllowingStateLoss() может привести к плохим впечатлениям для пользователя.Для получения дополнительной информации о том, почему выбрасывается это исключение, см. этот пост .

10 голосов
/ 21 февраля 2012

Я нашел грязное решение для такого рода проблем. Если вы по-прежнему хотите сохранить ActivityGroups по какой-либо причине (у меня были причины ограничения по времени), вы просто реализуете

public void onBackPressed() {}

в вашем Activity и введите туда некоторый код back. даже если на старых устройствах такого метода нет, этот метод вызывается более новыми.

6 голосов
/ 20 декабря 2016

Не используйте commitAllowingStateLoss (), его следует использовать только в тех случаях, когда можно нормально изменить состояние пользовательского интерфейса для пользователя.

https://developer.android.com/reference/android/app/FragmentTransaction.html#commitAllowingStateLoss()

Если транзакцияпроисходит в ChildFragmentManager для parentFragment, вместо этого используйте parentFragment.isResume () снаружи, чтобы проверить.

if (parentFragment.isResume()) {
    DummyFragment dummyFragment = DummyFragment.newInstance();
    transaction = childFragmentManager.BeginTransaction();
    trans.Replace(Resource.Id.fragmentContainer, startFragment);
}
5 голосов
/ 18 июля 2015

У меня была похожая проблема, сценарий был таким:

  • Моя активность - добавление / замена фрагментов списка.
  • Каждый фрагмент списка имеет ссылку на действие, чтобыуведомить действие при нажатии элемента списка (шаблон наблюдателя).
  • Каждый фрагмент списка вызывает setRetainInstance (true); в своем методе onCreate .

Метод onCreate действия был похож на это:

mMainFragment = (SelectionFragment) getSupportFragmentManager()
                .findFragmentByTag(MAIN_FRAGMENT_TAG);
        if (mMainFragment == null) {
            mMainFragment = new SelectionFragment();

            mMainFragment.setListAdapter(new ArrayAdapter<String>(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();
        }

Исключение было вызвано, поскольку при изменении конфигурации (устройство поворачивается),действие создано, основной фрагмент извлечен из истории диспетчера фрагментов, и в то же время фрагмент уже имеет OLD ссылку на уничтоженное действие

изменение реализации для решения этой проблемы:

mMainFragment = (SelectionFragment) getSupportFragmentManager()
                .findFragmentByTag(MAIN_FRAGMENT_TAG);
        if (mMainFragment == null) {
            mMainFragment = new SelectionFragment();

            mMainFragment.setListAdapter(new ArrayAdapter<String>(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);

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

...