Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Implements a method that allows a Fragment to be removed from anywhere in the back stack, using reflection to achieve this
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();
}
}
}
@kyominoh
Copy link

kyominoh commented Feb 4, 2015

i solved the problem
setValueToField(Fragment.class, removedFragment, "mChildFragmentManager", null);
above detachFragmentMethod in removeBackStackEntry
thank you shared source

@TimVelo
Copy link

TimVelo commented Oct 4, 2016

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