33
votes

Android platform: 3.1

I am trying to move a fragment from a container A to a container B. Here follows the code to accomplish this:

private void reattach(int newContainerId, Fragment frag, String tag) {

      if (frag == null || !frag.isAdded() || (frag.getId() == newContainerId)) { return; }

      final FragmentManager fm = getFragmentManager();
      FragmentTransaction ft = fm.beginTransaction();
      ft.remove(frag);     //stacco il frammento dal container A
      ft.commit();
      fm.executePendingTransactions();

      ft = fm.beginTransaction();
      ft.add(newContainerId, frag, tag); //attacco il frammento sul container D
      ft.commit();
      fm.executePendingTransactions();   
   }

When I run the system, I get the following IllegalStateException:

03-26 00:13:14.829: E/AndroidRuntime(30090): java.lang.RuntimeException: Unable to start activity ComponentInfo{eu.areamobile.apps.sfa/eu.areamobile.apps.sfa.activity.HomeActivity}: java.lang.IllegalStateException: Can't change container ID of fragment FragmentHomeController{408202a8 id=0x7f050010 HomeController}: was 2131034128 now 2131034132
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1751)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1767)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3117)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.access$1600(ActivityThread.java:122)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1009)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.os.Looper.loop(Looper.java:132)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.main(ActivityThread.java:4028)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at java.lang.reflect.Method.invokeNative(Native Method)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at java.lang.reflect.Method.invoke(Method.java:491)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:844)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at dalvik.system.NativeStart.main(Native Method)
03-26 00:13:14.829: E/AndroidRuntime(30090): Caused by: java.lang.IllegalStateException: Can't change container ID of fragment FragmentHomeController{408202a8 id=0x7f050010 HomeController}: was 2131034128 now 2131034132
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.BackStackRecord.doAddOp(BackStackRecord.java:338)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.BackStackRecord.add(BackStackRecord.java:316)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at eu.areamobile.apps.sfa.activity.HomeActivity.reattach(HomeActivity.java:340)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at eu.areamobile.apps.sfa.activity.HomeActivity.customHideShowCreate(HomeActivity.java:253)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at eu.areamobile.apps.sfa.activity.HomeActivity.customHideShowCreate(HomeActivity.java:155)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at eu.areamobile.apps.sfa.activity.HomeActivity.onPostCreate(HomeActivity.java:66)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.Instrumentation.callActivityOnPostCreate(Instrumentation.java:1111)
03-26 00:13:14.829: E/AndroidRuntime(30090):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1734)

After a quick debug, I noticed that on Android 3.1 the FragmentTransaction.remove doesn't set to 0 the mContainerId of the fragment being removed, while on ICS it works correctly.
Any suggestions or workarounds?

11
hi! i have the same problem. have you managed to get this working on systems prior ics?Anton

11 Answers

22
votes

My solution to this problem is to re-create the fragment while keeping its state:

FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.remove(old);
Fragment newInstance = recreateFragment(old);
ft.add(R.id.new_container, newInstance);
ft.commit();

With the following helper function:

private Fragment recreateFragment(Fragment f)
    {
        try {
            Fragment.SavedState savedState = mFragmentManager.saveFragmentInstanceState(f);

            Fragment newInstance = f.getClass().newInstance();
            newInstance.setInitialSavedState(savedState);

            return newInstance;
        }
        catch (Exception e) // InstantiationException, IllegalAccessException
        {
            throw new RuntimeException("Cannot reinstantiate fragment " + f.getClass().getName(), e);
        }
    }

It works for me, at least with the latest support library (r11), although I didn't test much yet.

The extra cost is to instantiate the fragment twice.

17
votes

after trying all the answers from similar questions, looks like i've found a way to do the trick.

First issue - you really have to commit and execute remove transaction before trying to add fragment to another container. Thanks for that goes to nave's answer

But this doesn't work every time. The second issue is a back stack. It somehow blocks the transaction.

So the complete code, that works for me looks like:

manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
manager.beginTransaction().remove(detailFragment).commit();
manager.executePendingTransactions();
manager.beginTransaction()
    .replace(R.id.content, masterFragment, masterTag)
    .add(R.id.detail, detailFragment, activeTag)
    .commit();              
2
votes

I had a similar problem and calling manager.executePendingTransactions() before adding the fragment for the second time did the trick for me. Thanks!

1
votes

This can happen when try to add the same fragment more than once.

    public void populateFragments() {
        Fragment fragment = new Fragment();
        //fragment is added for the first time
        addFragment(fragment);

        // fragment is added for the second time
        // This call will be responsible for the issue. The 
        addFragment(fragment);
    }

    public void addFragment(Fragment fragment) {
        FrameLayout frameLayout = AppView.createFrameLayout(context);
        view.addView(frameLayout);
        getSupportFragmentManager().beginTransaction().add(frameLayout.getId(), fragment).commit();
    }
1
votes

I found the problem was I needed two FragmentTransactions. I was trying to remove() and commit() twice on the same one.

 // create new fragment
 MyFragment myNewFragment = MyFragment.newInstance(this, data, true);
 FragmentManager fm = this.getSupportFragmentManager();
 // clear the back stack
 fm.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

 //fragmentTransaction is created for the removal of the old
 FragmentTransaction fragmentTransaction = fm.beginTransaction();
 fragmentTransaction.remove(myOldFragment);
 fragmentTransaction.commit();
 fm.executePendingTransactions();

 // fragmentTransaction2 is created to add the new one
 FragmentTransaction fragmentTransaction2 = fm.beginTransaction();
 fragmentTransaction2.add(R.id.fragment_frame, myNewFragment);
 fragmentTransaction2.commit();
 fm.executePendingTransactions();
0
votes

Based on Fragment Developer Guide which it states:

If you do not call addToBackStack() when you perform a transaction that removes a fragment, then that fragment is destroyed when the transaction is committed and the user cannot navigate back to it. Whereas, if you do call addToBackStack() when removing a fragment, then the fragment is stopped and will be resumed if the user navigates back

So I assume that your frag object is destroyed after the calls to

ft.remove(frag);     //stacco il frammento dal container A
ft.commit();

But you say that it works in ICS and this is strange. Why don't you try the replace method and see what's happens?

Hope this helps...

0
votes

Try to use replace() method..If not don't use commit twice.

0
votes

I'll need to prototype something later if this doesn't help but one thing I would try is not doing this as separate transactions because even though you call executePendingTransactions() it shouldn't be a blocking call, so while it's doing its thing the rest of your code will begin to execute, which includes trying to add the Fragment to a new container while it might still exist in its current container.

Since you're doing all of this from a single method, you could try doing the remove and add in a single FragmentTransaction, so that you know it will execute in the order that you want it to, and so that you know when the Fragment has been removed from one container and can add it to a new one and then commit. Else, if you really want to do it as two separate transactions, the base Fragment class has an isRemoving() method that will tell you if the Fragment is being removed from it's container and you can wait until that becomes false so that you know it's been removed. That said, you're starting to introduce synchronization issues if now one transaction depends on another before it can execute, and I don't see any reason not to just to the remove and add as a single transaction.

Thanks, David

0
votes

Is there any valid reason aside from poor programming principals, that you'd want to keep a reference to an existing fragment across an activity or two anyway? Rather than saving the state of the fragment and simply recreating it?

I was creating fragments using:

    public static ContactsFragment mContactsFragment;

    public static ContactsFragment getInstance() {
    if (mContactsFragment == null) {
        mContactsFragment = new ContactsFragment();
    }
    return mContactsFragment;
}

I've done this since I learned Android but can't see why I'd want to refer to my fragment statically despite it being in everyone's examples. Originally I thought it was to refer to a Context statically, but you can always set this when you create a new fragment and saving a context anyway often leads to odd bugs later anyway.

Why wouldn't you:

    public static ContactsFragment newInstance(Context c) {
    ContactsFragment mContactsFragment = new ContactsFragment();
    mContext = c;
    return mContactsFragment;
}

and simply save any work you've done on the fragment and recreate it when you need it again? Perhaps I can imagine if you were creating 100+ (news app?) you might not want to have to create them each time, rather store them in memory? Anyone?

0
votes

So, i had same issue, and solved the problem on a different way (at least a bit of). My App has 4 fragments and i need to manage when my device is on Landscape Mode.

let me explain my app and the solution:

My App i have 4 fragments in my app: 1 - Recipes (which contain a list of recipes) 2 - Ingredients (a list of ingredients from select recipe) 3 - Steps (list of steps) 4 - Player (a fragment that’s called to play videos)

On portrait mode i had no issue, all works fine!

Landscape My activity have 2 containers for my fragments, one on left side wich contains my list of recipes, and another on right side. This container on right side, will dinamically change .

I'm having this issue, when, on portrait mode, my fragment is showing, and when i rotate my device, i want the same fragment that was showing to go to detail container. And thats where all my problems started.

My Solution

  1. Save the last fragment showed in a variable;
  2. Save the state fragment onSavedInstance;
  3. In onCreate retrieve the fragment and the last fragment showed.

So, to be more especific i created this gist to help https://gist.github.com/jillesRagonha/4c184165b92fdf026ab3cd033b64b1bf

hope this help anyone :D

0
votes

Remove transaction.commitAllowingStateLoss(); if present (for previous fragments) and use transaction.commit(); instead.

Its better to keep a global function like this for all fragments/activities in BaseActivity/BaseFragment

protected void replaceMyFragment(Fragment fragment, int container_view_id, boolean addToBackStack, String tag) {
    // Begin the transaction
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    // Replace the contents of the container with the new fragment
    //ft.setCustomAnimations(R.anim.fade_in, R.anim.fade_out);
    ft.replace(container_view_id, fragment, tag);
    // or ft.add(R.id.your_placeholder, new FooFragment());
    if (addToBackStack) {
        ft.addToBackStack(tag);
    }
    // Complete the changes added above
    ft.commit();
}

And call them like:

replaceMyFragment(new YourFragment(),R.id.your_container,false,fragment.getTag());

in your activity / fragment.