Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IllegalStateException when .replace fragment on restart

I am using FragmentTransaction.replace() to swap fragments in and out. The app starts up first time with no problem. An IllegalStateException is thrown when rotating the device because of a conflict between the savedInstanceState and commiting a new fragment transaction. No AsyncTask is involved.

One StackOverflow question suggests to put the setContentView() call in onResumeFragments(), but this seems to have no effect. Same with onPostResume().

Another StackOverflow question says to override onConfigurationChanged(). This works in that sense that it the exception doesn't occur because the Activity is not restarted. However, this prevents fragments that have different portrait and landscape layouts from switching between these layouts. Calling setContentView() in onConfigurationChanged() causes a similar error (IllegalArgumentException: Binary XML file line #25: Duplicate id 0x12345678, tag null, or parent id with another fragment)

Using fragmentTransaction.commitAllowingStateLoss() instead of .commit() causes IllegalStateException: Activity has been destroyed.

How do I get this to work?

More exception info:

java.lang.RuntimeException: Unable to start activity ComponentInfo{myapp/myap.MainActivity}:
android.view.InflateException: Binary XML file line #25: Error inflating class fragment at myapp.MainActivity.onResumeFragments(MainActivity.java:450)
Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at > android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1533) at myapp.fragments.FragmentChange.onFragmentChange(FragmentChange.java:128) at myapp.MainActivity.onNavigationDrawerItemSelected(MainActivity.java:490) at myapp.fragments.NavigationDrawerFragment.selectItem(NavigationDrawerFragment.java:197) at myapp.fragments.NavigationDrawerFragment.onCreate(NavigationDrawerFragment.java:78) at myapp.MainActivity.onResumeFragments(MainActivity.java:450)

The sequence in the code upon rotating the device is:

MainActivity.onPause()  
MainActivity.saveInstanceState()  
NavigationDrawerFragment.onSaveInstanceState()  
MainActivity.onStop()
MainActivity.onDestroy()
MainActivity.onCreate()
    super.onCreate(savedInstanceState);
MainActivity.onResumeFragments()
    setContentView()
NavigationDrawerFragment.onCreate()
MainActivity.onNavigationDrawerItemSelected()
     fragmentTransaction.commit();

MainActivity:

public class MainActivity extends AppCompatActivity implements
        NavigationDrawerFragment.NavigationDrawerCallbacks {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }


    @Override
    public void onNavigationDrawerItemSelected(int position) {
        ...
        FragmentChangeEvent fragmentChangeEvent = new FragmentChangeEvent(null);
        FragmentChange fragmentChange = FragmentChange.getInstance( getSupportFragmentManager());
        fragmentChange.onFragmentChange(fragmentChangeEvent);
        ...
    }

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

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

        // causes onNavigationDrawerItemSelected() to be called, exception thrown 
        setContentView(myapp.R.layout.activity_main);

        mNavigationDrawerFragment = (NavigationDrawerFragment)
                getSupportFragmentManager().findFragmentById(myapp.R.id.navigation_drawer);

        mNavigationDrawerFragment.setUp( // Set up the drawer
                myapp.R.id.navigation_drawer,
                (DrawerLayout) findViewById(myapp.R.id.drawer_layout));
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if current fragment is "Individual" { // pseudocode
            setContentView(R.layout.activity_main); // causes IllegalArgumentException
        }
    }
}

NavigationDrawerFragment

public class NavigationDrawerFragment extends Fragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
        mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);

        if (savedInstanceState != null) {
            mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
            mFromSavedInstanceState = true;
        }

        // Select either the default item (0) or the last selected item.
        selectItem(mCurrentSelectedPosition);
    }

    private void selectItem(int position) {

        mCurrentSelectedPosition = position;
        if (mDrawerListView != null) {
            mDrawerListView.setItemChecked(position, true);
        }
        if (mDrawerLayout != null) {
            mDrawerLayout.closeDrawer(mFragmentContainerView);
        }
        if (mCallbacks != null) {
            // calls MainActivity.onNavigationDrawerItemSelected() 
            mCallbacks.onNavigationDrawerItemSelected(position);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition);
    }

}

FragmentChange

public class FragmentChange implements FragmentChangeListener {

      public static FragmentChange getInstance(FragmentManager fragmentManager) {
        if (instance == null) {
            instance = new FragmentChange(fragmentManager);
        }
        return instance;
    }

    // constructor
    private FragmentChange(FragmentManager fragmentManager) {
        mFragmentManager = fragmentManager;
    }

    @Override
    public void onFragmentChange(FragmentChangeEvent fragmentChangeEvent) {
        ...
        mPosition = fragmentChangeEvent.getPosition();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
        Fragment fragment = EmployeesVerticalFragment.newInstance();
        fragmentTransaction.replace(myapp.R.id.container, fragment);

        fragmentTransaction.commit(); // IllegalState exception here
        ...
    }
}

A greatly reduced form of the project on github which reproduces the IllegalStateException:

like image 578
Al Lelopath Avatar asked Nov 16 '16 18:11

Al Lelopath


3 Answers

Some comments:
I can see you have gone "above and beyond" trying to fix it (and have scanned stackoverflow for many tips).
Your code works the first time (even if you are in landscape b4 starting app).
The rotation fails to inflate the fragment (second time around) (in activity_main), and also reports the "commit after save" error (that's weird,how on Earth are we getting 2 fatal errors ? maybe on another thread, or ""save" is killing the inflator, but how can it carry on?).
NavigationDrawerFragment has a LOT of important code in it (that normally goes in the Activity).
Analysis:
Your MainActivity class extends AppCompatActivity:

public class MainActivity extends AppCompatActivity implements
        NavigationDrawerFragment.NavigationDrawerCallbacks {

Normally you extend FragmentActivity:

public class MainActivity extends FragmentActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {

This is because you inflate activity_main, which contains a fragment.

setContentView(com.example.replacefragments.R.layout.activity_main);

Inheritance: Activity <- FragmentActivity <- AppCompatActivity <- ActionBarActivity
'<-' means inheritance here.
One reason why you would need to consider FragmentActivity specifically is if you want to use nested fragments (a fragment holding another fragment), as that was not supported in native fragments until API Level 17.
I think this is what you are doing here (in activity_main):

<fragment android:id="@+id/navigation_drawer

Here is a good template app that should make your life easier:
android-sliding-menu-using-navigation-drawer/
FIXING IT:
Replace the fragment in activity_main.xml with the ListView.
I have discarded NavigationDrawerFragment, and injected some code from the template above, then re-introduced bit-by-bit your code from NavigationDrawerFragment, until it falls over. It now works with rotation, but not as you or I would like (preserving fragment state, NavigationDrawerFragment some functionality), so I'm still working on it.

protrait
protrait
landscape after rotation
landscape after rotation

Hot tip: The .commit sounds final does it not ? Sounds as if it should synchronous? Neither are true.
The .commit puts the transaction in a pending queue, to truly execute it you need:

getSupportFragmentManager().executePendingTransactions();

Here's a link to the error log of the posted code on rotation: link error log

like image 135
Jon Goodwin Avatar answered Oct 26 '22 16:10

Jon Goodwin


Method 1. Avoid config changes. Add android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard" in you manifest activity.

<activity
            android:name=".ui.accounts.YouActivity"
          android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard"
            android:theme="@style/AppTheme.NoActionBar" />

Method 2. a. First of all do not miss place code from default places like setContentView() in onConfigurationChanged().
b. Replace fragment with tag or by id in xml
c. Don't replace again if fragment found by id or tag.
d. Make sure you are dismissing any dailog inside fragment onPause.

like image 37
Qamar Avatar answered Oct 26 '22 17:10

Qamar


The FragmentManager is an

Interface for interacting with Fragment objects inside of an Activity.

It strikes me as a particular bad idea to have save it in a static field and reuse and old FragmentManager for a new activity. This will necessarily lead to Activity has been destroyed, when the new activity interact with the manager from the old activity.

In your code, replace

FragmentChange.getInstance(getFragmentManager());

by

new FragmentChange(getFragmentManager());
like image 39
rds Avatar answered Oct 26 '22 17:10

rds