Android multiple fragment transaction ordering

2019-01-17 22:42发布

问题:

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?

回答1:

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.



回答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.



回答3:

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++);


回答4:

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.



回答5:

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