12

I am getting this error below at the line of manager.popBackStack. Is there a way around this? It happens quite a it.

public void updateView(Fragment fragment) {

        IFragment currentFragment = (IFragment)fragment;

        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = manager.beginTransaction();
        fragmentTransaction.replace(R.id.content_frame, fragment);

        if(currentFragment != null)
        {
            if(currentFragment.isRoot())
            {
                manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
            }
            else
            {
                fragmentTransaction.addToBackStack("test");
            }
        }

        fragmentTransaction.commitAllowingStateLoss();

        if(drawerManager.DrawerLayout != null) {
            drawerManager.DrawerLayout.closeDrawer(drawerManager.DrawerList);
        }
    }
 Fatal Exception: java.lang.IllegalStateException: Can not perform this
 action after onSaveInstanceState
        at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2044)
        at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2067)
        at android.support.v4.app.FragmentManagerImpl.popBackStack(FragmentManager.java:799)
        at com.exposure.activities.BaseActivity.updateView(BaseActivity.java:239)
        at com.exposure.activities.events.EventActivity.setupEvent(EventActivity.java:204)
        at com.exposure.activities.events.EventActivity.getData(EventActivity.java:117)
        at com.exposure.utilities.ActivityContainer.getData(ActivityContainer.java:83)
        at com.exposure.utilities.DataTask.onPostExecute(DataTask.java:37)
        at android.os.AsyncTask.finish(AsyncTask.java:695)
        at android.os.AsyncTask.-wrap1(Unknown Source)
        at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:712)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6938)
       at java.lang.reflect.Method.invoke(Method.java)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
Sambhav Khandelwal
  • 3,585
  • 2
  • 7
  • 38
Mike Flynn
  • 22,342
  • 54
  • 182
  • 341

4 Answers4

16

As explained in Fragment Transactions & Activity State Loss under the bullet point "Avoid performing transactions inside asynchronous callback methods":

Avoid performing transactions inside asynchronous callback methods. This includes commonly used methods such as AsyncTask#onPostExecute() and LoaderManager.LoaderCallbacks#onLoadFinished(). The problem with performing transactions in these methods is that they have no knowledge of the current state of the Activity lifecycle when they are called.

This appears to be the issue that you are having since updateView() is called from an asynchronous task, but let's test that hypothesis.

The following demo app creates a fragment, simulates background processing and a callback that mimics your async callback. There is a flag in the code, mFixIt when set to true causes the app to behave properly (not blow up) and when false lets the app fail.

With mFixIt == false. The trigger is the home button that causes the app to go into a stopped state:

enter image description here

Here is the stack trace:

14967-15003 E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.example.illegalstatepopbackstack, PID: 14967
    java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
        at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2080)
        at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2106)
        at android.support.v4.app.FragmentManagerImpl.popBackStack(FragmentManager.java:832)
        at com.example.illegalstatepopbackstack.MainActivity.updateView(MainActivity.java:70)
        at com.example.illegalstatepopbackstack.ui.main.MainFragment$1.run(MainFragment.java:63)
        at java.lang.Thread.run(Thread.java:764)

Now with mFixIt == true. The difference this time is that the app recognizes an async callback while the activity is in a stopped state, records that this has happened and completes the processing when the app is restarted. The visual is simply pressing the home button and restoring from "recents". The app simply puts up the fragment at the start and when re-started changes the top TextView text and pops the fragment from the backstack.

enter image description here

As can be seen, processing completes as expected.

This is a trivial example. If your processing is more involved or you just want a more formal way of handling this situation I recommend taking a look at this solution.

Here is the code for the demo app:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    // Set to true to fix the problem; false will cause the IllegalStateException
    private boolean mFixIt = false;

    private MainFragment mFragment;
    private TextView mTextView;
    private boolean mIsPaused;
    private boolean mUpdateViewNeeded;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        mTextView = findViewById(R.id.textView);

        FragmentManager fm = getSupportFragmentManager();
        if (savedInstanceState == null) {
            // Create out fragment
            mFragment = MainFragment.newInstance();
            fm.beginTransaction()
                .replace(R.id.container, mFragment)
                .addToBackStack(FRAGMENT_TAG)
                .commit();
        } else {
            // Find the restored fragment.
            mFragment = (MainFragment) fm.findFragmentByTag(FRAGMENT_TAG);
        }
    }

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

        // Simulate a background task that does something useful. This one just waits a few
        // second then does a callback to updateView(). The activity will be fully paused by then.
        mFragment.doSomethingInBackground();
        mIsPaused = true;
        Log.d("MainActivity","<<<< stopped");
    }

    @Override
    protected void onStart() {
        super.onStart();
        mIsPaused = false;
        if (mUpdateViewNeeded) {
            // Execute delayed processing now that the activity is resumed.
            updateView(getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG));
        }
    }

    public void updateView(Fragment fragment) {
        if (mIsPaused && mFixIt) {
            // Delay processing
            mUpdateViewNeeded = true;
        } else {
            // Do out update work. If we are paused, this will get an IllegalStateException. If
            // we are resumed, this will work as intended.
            mTextView.setText("Replaced...");
            getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
            mUpdateViewNeeded = false;
        }
    }

    public static final String FRAGMENT_TAG = "MyFragment";
}

MainFragment.java

public class MainFragment extends Fragment {

    MainActivity mMainActivity;

    public static MainFragment newInstance() {
        return new MainFragment();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.main_fragment, container, false);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mMainActivity = (MainActivity) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mMainActivity = null;
    }

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

    }

    public void doSomethingInBackground() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (mMainActivity != null) {
                    mMainActivity.updateView(MainFragment.this);
                }
            }
        }).start();
    }
}

main_activity.xml

<TextView
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_light"
    android:gravity="center"
    android:text="To be replaced..."
    android:textSize="36sp"
    android:textStyle="bold" />

<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

main_fragment.xml

<android.support.constraint.ConstraintLayout 
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_green_light"
    tools:context=".ui.main.MainFragment">

    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MainFragment"
        android:textSize="36sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • I basically did what you did, add a bit on onStop or onPause, and store the Fragment if paused for onStart to run again. – Mike Flynn Dec 07 '18 at 06:10
8

This is what worked for me is to check if fragment manger doesn't hava a saved state before popBackStack()

if (fragmentManager != null && !fragmentManager.isStateSaved()) {
    fragmentManager.popBackStack();
}
Sambhav Khandelwal
  • 3,585
  • 2
  • 7
  • 38
Melad
  • 1,184
  • 14
  • 18
3

Just call popBackStackImmediate() as the regular popBackStack() is asynchronous...

Janeviemus
  • 41
  • 4
1

the problem basically is:

IllegalStateException: Can not perform this action after onSaveInstanceState.

therefore, check with !isFinishing() && !isDestroyed() in .updateView()

because this would return true upon onSaveInstanceState() ...

alike this the situation can be mitigated and the crash prevented.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • If its not finishing what exactly will it do, nothing? Do I rerun something? – Mike Flynn Dec 04 '18 at 14:22
  • @MikeFlynn when the `BaseActivity` is `! isFinishing()`, the code should run - else not, because it runs in a situation, where it should not run and therefore crashes. `onSaveInstanceState()` is not being called when explicitly calling `finish()`, which should be considered there; however `isFinishing()` still should be true - and updating the view is pointless then. – Martin Zeitler Dec 04 '18 at 14:33
  • Didnt work, wrapped it in the statement, and still bypasses it – Mike Flynn Dec 04 '18 at 15:50
  • @MikeFlynn it might already be `isDestroyed()`; that's the state which follows up. you could set a `boolean` upon `onSaveInstanceState()` by yourself and then check for that; in case it still not works. the error message clearly hints for, that the operation is being performed during an illegal state, after that method. – Martin Zeitler Dec 04 '18 at 18:41
  • With this solution you can avoid the fatal exception, but, I get an W/System.err: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.view.View.getVisibility()' on a null object reference. – moralejaSinCuentoNiProverbio Sep 30 '19 at 12:46
  • just check if `view != null`... while this likely has little / nothing to do with this question and answer. – Martin Zeitler Oct 01 '19 at 03:29