13

I have a HorizontalScrollView containing a (horizontal) LinearLayout which I use as the container for adding multiple fragments. Upon some changes, I need to remove all fragments from that container and add new ones. However, there seems to be a problem with ordering when I'm removing the old fragments.

Here are the scenarios:

  • app startup
    • correctly adding fragments A1,B1,C1,D1 in this order
  • change content
    • if not removing initial fragments, but adding A2,B2,C2 (as a single transaction), it will show A1,B1,C1,D1,A2,B2,C2
    • if removing initial fragments (either as a separate or using the same transaction), then adding A2,B2,C2, it will show C2,B2,A2

For now I found a workaround, where I'm adding the new fragments first then removing the old ones (still as part of the same transaction) and that is working properly.

EDIT: The workaround doesn't work all the time.

I'm using android.support.v4.app.Fragment.

Any ideas on what's happening?

5 Answers 5

11

I enabled debugging on the FragmentManager and I found the problem.

Here's an excerpt from the logs, notice how the fragment index is allocated in reverse order:

V/FragmentManager? Freeing fragment index TimeTracesChartFragment{42ac4910 #7 id=0x7f080044}
V/FragmentManager? add: RealTimeValuesFragment{42a567b0 id=0x7f080044}
V/FragmentManager? Allocated fragment index RealTimeValuesFragment{42a567b0 #7 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d35c38 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d35c38 #6 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d35e98 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d35e98 #5 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d36220 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d36220 #4 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d39d18 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d39d18 #3 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d3a170 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d3a170 #2 id=0x7f080044}
V/FragmentManager? add: TimeTracesChartFragment{42d3a528 id=0x7f080044}
V/FragmentManager? Allocated fragment index TimeTracesChartFragment{42d3a528 #1 id=0x7f080044}
V/FragmentManager? moveto CREATED: TimeTracesChartFragment{42d3a528 #1 id=0x7f080044}

And here is the culprit code:

http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/support/v4/app/FragmentManager.java#FragmentManagerImpl.makeActive%28android.support.v4.app.Fragment%29

 void makeActive(Fragment f) {
        if (f.mIndex >= 0) {
            return;
        }

        if (mAvailIndices == null || mAvailIndices.size() <= 0) {
            if (mActive == null) {
                mActive = new ArrayList<Fragment>();
            }
            f.setIndex(mActive.size(), mParent);
            mActive.add(f);

        } else {
            f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
            mActive.set(f.mIndex, f);
        }
        if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
    }

Notice how the available indices are taken from the back of the list. It should probably choose the lowest index available so that it would preserve ordering.

Now to think of a workaround...

EDIT:

Here's a workaround: Create two separate transactions, one for the removals and then one for the additions, then do this:

removalTxn.commit();
getSupportFragmentManager().executePendingTransactions();
FragmentTransactionBugFixHack.reorderIndices(getSupportFragmentManager());
//create additionTxn
additionTxn.commit();

Where FragmentTransactionBugFixHack looks like this:

package android.support.v4.app;

import java.util.Collections;

public class FragmentTransactionBugFixHack {

    public static void reorderIndices(FragmentManager fragmentManager) {
        if (!(fragmentManager instanceof FragmentManagerImpl))
            return;
        FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
        if (fragmentManagerImpl.mAvailIndices != null)
            Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
    }

}

It's not ideal, because of the two separate transaction it will flicker to white (or whatever your container's background is), but at least it will order them properly.

4
  • Filed bug here: code.google.com/p/android/issues/…
    – radu
    May 7, 2014 at 17:07
  • 3
    I had similar bug, but my fragments was restored in wrong order after screen rotation (fragments from the bottom appears on the top and so on). I've placed FragmentTransactionBugFixHack to the android.support.v4.app package in my source code, and call: FragmentTransactionBugFixHack.reorderIndices(getSupportFragmentManager()); at the activity's onRestoreInstanceState. It fixes the order of fragments before fragments rendered. Thanks for the fix! Feb 19, 2015 at 12:34
  • @radu is there anyway we can adapt this solution for the andorid.app package rather than the support package? I have tried to do this but FragmentManagerImpl can not be found in android.app :( Thanks Jul 15, 2015 at 12:13
  • I had the same bug as Andrey Uglev, but my fragments had their own fragments (added via their child fragment managers). I added an answer below that includes some recursive code to go through all the fragment's child fragments, and those fragment's fragments, etc.
    – Rock Lee
    Apr 4, 2016 at 20:08
2

other way to fix this issue:

replace ArrayList mAvailIndices by ReverseOrderArrayList

ReverseOrderArrayList.java

public class ReverseOrderArrayList<T extends Comparable> extends ArrayList<T> {
@Override
public boolean add(T object) {
    boolean value = super.add(object);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

@Override
public void add(int index, T object) {
    super.add(index, object);
    Collections.sort(this, Collections.reverseOrder());
}

@Override
public boolean addAll(Collection<? extends T> collection) {
    boolean value = super.addAll(collection);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

@Override
public boolean addAll(int index, Collection<? extends T> collection) {
    boolean value = super.addAll(index, collection);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

@Override
protected void removeRange(int fromIndex, int toIndex) {
    super.removeRange(fromIndex, toIndex);
    Collections.sort(this, Collections.reverseOrder());
}

@Override
public boolean remove(Object object) {
    boolean value = super.remove(object);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

@Override
public boolean removeAll(Collection<?> collection) {
    boolean value = super.removeAll(collection);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

@Override
public T remove(int index) {
    T value = super.remove(index);
    Collections.sort(this, Collections.reverseOrder());
    return value;
}

}

Hack

public class FragmentTransactionBugFixHack {
private static final String TAG = "FragmentTransactionBugFixHack";

public static void injectFragmentTransactionAvailIndicesAutoReverseOrder(FragmentManager fragmentManager) {
    try {
        Log.d(TAG, "injection injectFragmentTransactionAvailIndicesAutoReverseOrder");
        if (fragmentManager==null || !(fragmentManager instanceof FragmentManagerImpl)) return;
        FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
        if (fragmentManagerImpl.mAvailIndices!=null && fragmentManagerImpl.mAvailIndices instanceof ReverseOrderArrayList) return;
        ArrayList<Integer> backupList = fragmentManagerImpl.mAvailIndices;
        fragmentManagerImpl.mAvailIndices = new ReverseOrderArrayList<>();
        if (backupList!=null) {
            fragmentManagerImpl.mAvailIndices.addAll(backupList);
        }
        Log.d(TAG, "injection ok");
    } catch (Exception e) {
        Log.e(TAG, e);
    }
}}

Using: call FragmentTransactionBugFixHack.injectFragmentTransactionAvailIndicesAutoReverseOrder in activity-onCreate.

1

As an alternative workaround, you could try adding views to the LinearLayout and then add each fragment to the correct view. It's not ideal, but it seems as though you cannot rely on the ordering of Fragment creation. Something like the following:

ViewGroup f1 = new ViewGroup(this);
linearLayout.addView(f1);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(f1, new A2(), "mediocreworkaround");
ft.commit();

Then later, when you remove all fragments, ensure you remove the corresponding views too.

linearlayout.removeAllViews();

Note: the code may syntactically wrong, I just typed this straight into StackOverflow. The idea is sound, albeit an average solution. Interesting question though - I will probably look into this more when I get more time. Find out exactly what happens. If I do, I'll post more information here.

Edit:

Rotation should be easy to handle if you let the system handle it for you. Add a unique id to each of the generated views.

//Declare a counter to ensure generated ids are different
idCounter = 1;

Now use that counter to set the ids when creating the views:

//Set unique id on the view. If you really want, you can do a 
//findViewById(idCounter) == null check to ensure it's uniqueness. 
//Once you add this id, the system will take care of remembering 
//it's state for you across configuration chagne
f1.setId(idCounter++);
7
  • That's an interesting idea, I'll give it a shot.
    – radu
    May 6, 2014 at 22:35
  • This seems to be working, but it's too much work to handle rotation as well - I'd need to save the generated view ids and restore them, or else it doesn't know where to attach the fragments...I'll try to hunt the problem down, maybe there's another workaround.
    – radu
    May 6, 2014 at 22:52
  • Edited post to handle orientation changes too May 6, 2014 at 23:01
  • Your approach to view ids is not safe. I used this instead to generate them: stackoverflow.com/questions/1714297/…. Saving them in onSaveInstanceState, then recreating them in onCreate. It all works but there's some nasty flickering (the main container goes white, then fragments get added). Still looking for a better solution, but this is a working (if ugly) workaround.
    – radu
    May 6, 2014 at 23:17
  • You should only need to add the fragments once (the first time). As long as your views have ids, they will not need to be created and added again. In fact, by doing that, you are adding more and more duplicate fragments into the FragmentManager. There is no need to override onSaveInstanceState unless you have member variables that you need to keep track of May 7, 2014 at 0:59
1

Had a similar issue, the solution I ended up using was to have multiple transactions. In my case, it was only A, B, C. And I used one transaction to add A, one to add B, one to add C.

The order of transactions seems to be reliable.

Probably requires more complex code, if you wish to have backstack working. But a backstack tag on the first transaction should allow proper handling there, too.

2
  • Unfortunately I did try a transaction for each fragment and that still didn't reliably work.
    – radu
    May 7, 2014 at 15:37
  • Strange, I had same issue and it almost killed me! This solution works for me anyway: one transaction at a time fm.beginTransaction().add(layout, frag, tag).commit();
    – Ge Rong
    Dec 26, 2014 at 12:49
0

Here is a slightly-modified version of Radu's answer where I added the recursion part at the end. This reorders indices of the given fragment manager, and all its fragments' childFragmentMangers, and all of those fragments' childFragmentManagers, and so on.

This is a new class that you add to your project (You can add a package android.support.v4.app to your source code's java folder, and put it in that package, and that worked for me):

package android.support.v4.app;

public class FragmentTransactionBugFixHack {

    public static void reorderIndices(FragmentManager fragmentManager) {
        if (!(fragmentManager instanceof FragmentManagerImpl))
            return;
        FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
        if (fragmentManagerImpl.mAvailIndices != null) {
            Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
        }

        //Recursively reorder indices of all child fragments.
        List<Fragment> fragments = fragmentManager.getFragments();
        //The support library FragmentManager returns null if none.
        if(fragments != null) {
            for (Fragment fragment : fragments) {
                //For some reason, the fragments in the list of fragments might be null.
                if(fragment != null) {
                    reorderIndices(fragment.getChildFragmentManager());
                }
            }
        }
    }
}

For solving the problem of when it recreates fragments out of order when the device is rotated, just put this in your Activity class which manages the fragment (Credit goes to Andrey Uglev's comment in Radu's answer):

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    FragmentTransactionBugFixHack.reorderIndices(getSupportFragmentManager());
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.