Implements a method that allows a Fragment to be removed from anywhere in the back stack, using reflection to achieve this
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.Random; | |
import android.graphics.Color; | |
import android.os.Bundle; | |
import android.support.v4.app.Fragment; | |
import android.support.v4.app.FragmentActivity; | |
import android.support.v4.app.FragmentManager; | |
import android.support.v4.app.FragmentManager.BackStackEntry; | |
import android.util.Log; | |
import android.view.Gravity; | |
import android.view.LayoutInflater; | |
import android.view.View; | |
import android.view.View.OnClickListener; | |
import android.view.ViewGroup; | |
import android.widget.Button; | |
import android.widget.FrameLayout; | |
import android.widget.LinearLayout; | |
import android.widget.TextView; | |
import android.widget.Toast; | |
/** | |
* Implements a method that allows a Fragment to be removed from anywhere in the | |
* back stack, <br/> | |
* using reflection to achieve this.<br/> | |
* <br/> | |
* Although this code was tested, it was written as an exercise and not to be | |
* used in a real-world app. <br/> | |
* Someting may be broken, so re-test, explore the code, see if anyone thought | |
* of a better way to achive this, <br/> | |
* and rethink if you really need this before using it.<br/> | |
* <br/> | |
* Developed and tested with revision 19.0.1 of the support-v4 library | |
* | |
* @author Dejan Jankov | |
*/ | |
public class FragmentBackStackModifyActivity extends FragmentActivity { | |
private static final String TAG = "BackStackModify"; | |
private static final int FRAME_ID = 7256453; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
if (savedInstanceState == null) { | |
Toast.makeText(getApplicationContext(), | |
"popBackStack from FragmentManager is used when the backstack size <= 1 or", Toast.LENGTH_LONG) | |
.show(); | |
Toast.makeText(getApplicationContext(), "the Fragment to be removed is the visible one", Toast.LENGTH_LONG) | |
.show(); | |
Toast.makeText(getApplicationContext(), "Add 2 or more to remove random Fragment", Toast.LENGTH_LONG) | |
.show(); | |
} | |
LinearLayout container = new LinearLayout(this); | |
container.setOrientation(LinearLayout.VERTICAL); | |
Button addNewFragment = new Button(this); | |
addNewFragment.setText("Add new Fragment"); | |
addNewFragment.setOnClickListener(new OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
performAddNewFragment(); | |
} | |
}); | |
container.addView(addNewFragment); | |
Button removeRandomFragment = new Button(this); | |
removeRandomFragment.setText("Remove random from backstack"); | |
removeRandomFragment.setOnClickListener(new OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
performRemoveRandomFragment(); | |
} | |
}); | |
container.addView(removeRandomFragment); | |
FrameLayout frame = new FrameLayout(this); | |
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, | |
ViewGroup.LayoutParams.MATCH_PARENT); | |
frame.setLayoutParams(lp); | |
frame.setId(FRAME_ID); | |
container.addView(frame); | |
setContentView(container); | |
} | |
public void performAddNewFragment() { | |
int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount(); | |
getSupportFragmentManager() // | |
.beginTransaction() // | |
.replace(FRAME_ID, TextFragment.newInstance(backStackEntryCount)) // | |
.addToBackStack(backStackEntryCount + "") // | |
.commit(); | |
} | |
public void performRemoveRandomFragment() { | |
try { | |
@SuppressWarnings("unchecked") | |
ArrayList<BackStackEntry> backStack = (ArrayList<BackStackEntry>) getValueFromField(null, | |
getSupportFragmentManager(), "mBackStack"); | |
String indexFrToRemove = "0"; | |
if (backStack != null && backStack.size() > 1) { | |
int backStackSize = backStack.size(); | |
indexFrToRemove = String.valueOf(new Random().nextInt(backStackSize)); | |
for (int i = 0; backStack != null && i < backStackSize; i++) { | |
BackStackEntry entry = backStack.get(i); | |
if (entry.getName().equals(indexFrToRemove)) { | |
// retrieve the index of the backstack record from the | |
// list | |
int bsrIndex = (Integer) getValueFromField(null, entry, "mIndex"); | |
removeBackStackEntry(backStack, bsrIndex); | |
Log.d(TAG, backStack.size() + " records left in the back stack"); | |
return; | |
} | |
} | |
} else if (backStack != null && backStack.size() != 0) { | |
removeBackStackEntry(backStack, 0); | |
return; | |
} | |
Toast.makeText(getApplicationContext(), | |
backStack == null ? "Backstack empty" : "Could not find Fragment with name " + indexFrToRemove, | |
Toast.LENGTH_SHORT).show(); | |
} catch (Exception e) { | |
// not handled | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* Removes the Fragment with index of {@code index} from the back stack | |
* | |
* @param backStack | |
* the backstack list | |
* @param index | |
* the index of the Fragment to be removed in {@code backStack} | |
*/ | |
@SuppressWarnings("unchecked") | |
private void removeBackStackEntry(List<BackStackEntry> backStack, final int index) { | |
FragmentManager fm = getSupportFragmentManager(); | |
if (backStack.size() - 1 <= 1 || backStack.size() - 1 == index) { | |
Toast.makeText(getApplicationContext(), "popBackStack of FramentManager invoked", Toast.LENGTH_SHORT) | |
.show(); | |
Log.i(TAG, "Invoked popBackStack on manager"); | |
fm.popBackStack(); | |
return; | |
} | |
if (backStack.size() <= index || index < 0) { | |
Toast.makeText(getApplicationContext(), "Fragment index invalid", Toast.LENGTH_SHORT).show(); | |
Log.i(TAG, "Fragment index invalid: " + index); | |
return; | |
} | |
synchronized (fm) { | |
// FragmentManagerImpl#mActive | |
List<Fragment> activeFragments = fm.getFragments(); | |
if (index < 0 || index > activeFragments.size() || activeFragments.get(index) == null) { | |
Log.d(TAG, "Index " + index + " not valid"); | |
return; | |
} | |
int backStackSize = backStack.size(); | |
for (int i = index; i < backStackSize; i++) { | |
// fm relies on mIndex for saving the state of the fragment, | |
// showing the next/previous, etc. | |
// we need to change their index values | |
// because we're going to remove one fragment from the backstack | |
int newIndex = i - 1; | |
Log.i(TAG, "Reseting mIndex of " + i + " to " + newIndex); | |
Fragment tmpFr = activeFragments.get(i); | |
BackStackEntry tmpBse = backStack.get(i); | |
if (tmpFr != null) { | |
setValueToField(Fragment.class, activeFragments.get(i), "mIndex", newIndex); | |
} else { | |
// should never happen | |
Log.d(TAG, "Fragment for index " + i + " is null"); | |
continue; | |
} | |
if (tmpBse != null) { | |
setValueToField(null, backStack.get(i), "mIndex", newIndex); | |
} else { | |
// should never happen | |
Log.d(TAG, "BackStackEntry for index " + i + " is null"); | |
} | |
} | |
// remove the fragment from the backstack list | |
BackStackEntry removedEntry = backStack.remove(index); | |
int removedEntryIndex = index; | |
// BackStackRecord#popFromBackStack uses the 'removed' fragment list | |
// to go back to the previous fragment. | |
// here we set the list of 'removed' fragments from the fragment | |
// to-be-removed, to the next one in the backstack | |
Object removedEntryOpHead = getValueFromField(null, removedEntry, "mHead"); | |
Object removedOpRemovedFragment = getValueFromField(null, removedEntryOpHead, "removed"); | |
Object nextEntryOpHead = getValueFromField(null, backStack.get(removedEntryIndex), "mHead"); | |
setValueToField(null, nextEntryOpHead, "removed", removedOpRemovedFragment); | |
Object nextEntryOpTail = getValueFromField(null, backStack.get(removedEntryIndex), "mTail"); | |
setValueToField(null, nextEntryOpTail, "removed", removedOpRemovedFragment); | |
Log.i(TAG, "Fragments 'removed' list set to next backstack record"); | |
Fragment removedFragment = activeFragments.get(index); | |
try { | |
// perform destroy one the fragment before removing it | |
Method performDestoryMethod = Fragment.class.getDeclaredMethod("performDestroy"); | |
performDestoryMethod.setAccessible(true); | |
performDestoryMethod.invoke(removedFragment); | |
Log.i(TAG, "Invoked performDestory on fragment"); | |
// FragmentManagerImpl#mAvailBackStackIndices | |
List<Integer> availableIndices = (List<Integer>) getValueFromField(fm.getClass(), fm, | |
"mAvailBackStackIndices"); | |
// FragmentManagerImpl#mBackStackIndices | |
List<BackStackEntry> backStackIndices = (List<BackStackEntry>) getValueFromField(fm.getClass(), fm, | |
"mBackStackIndices"); | |
// we need to know where to store the fragment and it's | |
// backstackrecord so we can use the manager to make the | |
// fragment inactive and mark it's index as free. | |
// this will place the objects after the last non-null item in | |
// the lists | |
int nextAvailableIndex = backStackIndices.size() - 1; | |
if (availableIndices != null && availableIndices.size() > 0) { | |
nextAvailableIndex = availableIndices.get(availableIndices.size() - 1); | |
nextAvailableIndex = Math.min(backStackIndices.size() - 1, nextAvailableIndex - 1); | |
} else if (backStackIndices.size() < activeFragments.size()) { | |
// this can happen if the parent activity was restarted for | |
// example. now 'activeFragments' holds some null values but | |
// 'backStackIndices' holds only non-null values and | |
// 'availableIndices' is empty. | |
// because 'backStackIndices' does not hold any null values | |
// we know that we should place the fragment on the back of | |
// that list | |
nextAvailableIndex = backStackIndices.size() - 1; | |
} | |
// move the fragment to be back of the list | |
activeFragments.remove(index); | |
activeFragments.add(nextAvailableIndex, removedFragment); | |
// modify the 'mIndex' field to match the position in the list | |
// so we can safely call `makeInactive` | |
setValueToField(Fragment.class, removedFragment, "mIndex", nextAvailableIndex); | |
Log.i(TAG, "Moved the fragment to the back of the list"); | |
// notify the fragment that it's detached | |
removedFragment.onDetach(); | |
// move the bsr to be back of the list | |
backStackIndices.remove(index); | |
backStackIndices.add(nextAvailableIndex, removedEntry); | |
Log.i(TAG, "Moved the backstack record to the back of the list"); | |
// free the index we used for the removed items | |
Method freeIndexMethod = fm.getClass().getDeclaredMethod("freeBackStackIndex", int.class); | |
freeIndexMethod.setAccessible(true); | |
freeIndexMethod.invoke(fm, nextAvailableIndex); | |
Log.i(TAG, "Freed backstack index"); | |
// the frament needs to be active to be detached by | |
// detachFragment | |
setValueToField(Fragment.class, removedFragment, "mAdded", true); | |
Method detachFragmentMethod = fm.getClass().getDeclaredMethod("detachFragment", Fragment.class, | |
int.class, int.class); | |
detachFragmentMethod.invoke(fm, removedFragment, 0, 0); | |
Log.i(TAG, "Fragment detached"); | |
// invalidate the fragment. | |
// removes it from the 'mActive' list, adds the index to the | |
// available indices list, destroys loaders, etc. | |
Method makeInactiveMethod = fm.getClass().getDeclaredMethod("makeInactive", Fragment.class); | |
makeInactiveMethod.setAccessible(true); | |
makeInactiveMethod.invoke(fm, removedFragment); | |
Log.i(TAG, "Fragment made inactive"); | |
// clear references of the activity and manager | |
setValueToField(Fragment.class, removedFragment, "mActivity", null); | |
setValueToField(Fragment.class, removedFragment, "mFragmentManager", null); | |
Toast.makeText(getApplicationContext(), "Fragment with index " + index + " removed from back stack", | |
Toast.LENGTH_SHORT).show(); | |
} catch (Exception e) { | |
// not handled, like every other exception | |
e.printStackTrace(); | |
Toast.makeText( | |
getApplicationContext(), | |
"Fragment with index " + index + " could not be removed from the back stack, reason: " | |
+ e.getMessage(), Toast.LENGTH_SHORT).show(); | |
} | |
} | |
} | |
/** | |
* | |
* @param clazz | |
* will be read from {@code fieldObject} if null | |
* @param fieldObject | |
* @param fieldName | |
* @param value | |
*/ | |
private void setValueToField(Class<?> clazz, Object fieldObject, String fieldName, Object value) { | |
if (clazz == null) { | |
clazz = fieldObject.getClass(); | |
} | |
try { | |
Field field = clazz.getDeclaredField(fieldName); | |
field.setAccessible(true); | |
field.set(fieldObject, value); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* | |
* @param clazz | |
* will be read from {@code fieldObject} if null | |
* @param fieldObject | |
* @param fieldName | |
* @return | |
*/ | |
private Object getValueFromField(Class<?> clazz, Object fieldObject, String fieldName) { | |
if (fieldObject == null) { | |
return null; | |
} | |
if (clazz == null) { | |
clazz = fieldObject.getClass(); | |
} | |
try { | |
Field field = clazz.getDeclaredField(fieldName); | |
field.setAccessible(true); | |
return field.get(fieldObject); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return null; | |
} | |
public static class TextFragment extends Fragment { | |
public static Fragment newInstance(int index) { | |
Bundle args = new Bundle(); | |
args.putInt("fragmentIndex", index); | |
Fragment fr = new TextFragment(); | |
fr.setArguments(args); | |
return fr; | |
} | |
public TextFragment() { | |
} | |
@Override | |
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | |
TextView textView = new TextView(getActivity()); | |
textView.setTextColor(Color.WHITE); | |
textView.setTextSize(22); | |
textView.setBackgroundColor(Color.GRAY); | |
textView.setGravity(Gravity.CENTER); | |
int index = getIndex(); | |
textView.setText(index + ": " + System.currentTimeMillis() + ""); | |
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, | |
ViewGroup.LayoutParams.MATCH_PARENT); | |
textView.setLayoutParams(lp); | |
return textView; | |
} | |
private int getIndex() { | |
return getArguments().getInt("fragmentIndex"); | |
} | |
@Override | |
public void onDetach() { | |
Log.v("Fragment", "[onDetach] " + getIndex()); | |
super.onDetach(); | |
} | |
@Override | |
public void onDestroy() { | |
Log.v("Fragment", "[onDestroy] " + getIndex()); | |
super.onDestroy(); | |
} | |
@Override | |
protected void finalize() throws Throwable { | |
Log.v("Fragment", "[finalize] " + getIndex()); | |
super.finalize(); | |
} | |
} | |
} |
Found a difference in my code
I used fragment.add(), occur invocation target exception in removeBackStackEntry method (by invoke calling)
so After a trace of the original source (FragmentMasterImpl)
FragmentManager.mActivity that was null and An exception was thrown
I dont have any idea
Are you able to modify?
i solved the problem
setValueToField(Fragment.class, removedFragment, "mChildFragmentManager", null);
above detachFragmentMethod in removeBackStackEntry
thank you shared source
Awesome! Thanks for the hard work and documentation on this gist... This totally got me 99 yards down the field. You get 5 internets.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing your code. I try to use it with Three Fragment (A,B,C Fragments)
but removeBackStackEntry parameter is final int index..
i use addToBackStrack fragmentName string instead of back stack count;
how to replace your code?