Since Tab Activity is deprecated I'm trying to implement tabs with fragments. As you can see in the lots of StackOverFlow questions back stack is an issue when you work with fragments and which has its own back stack.
So the thing I'm trying to do is, there is a fragment in each tab and this fragment can call another fragment within the same tab and its also the same for the other tabs.
Since there is only one activity then there is only one back stack for whole application. So I need to create my custom back stack which is separated for each tab. It's also the same common idea in the other questions. I need to find a way to create custom back stack but I couldnt find any example to take a look.
Is there any tutorial or any example piece of code doing something similar ? Thanks in advance.
There is a backstack for the whole application, but there is also a backstack for fragments.
Perhaps have a read of this:
http://developer.android.com/guide/components/fragments.html#Transactions
When you perform a fragment transaction (add, replace or remove), you can add that transaction to the backstack.
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragmentContainer, fragment);
ft.addToBackStack(null);
ft.commit();
And now when you press back, the latest fragment will be 'popped' from the fragment backstack.
You could also override the onBackPressed(), and manage your fragment backstack there. (I'm currently having trouble trying to work out how to do this efficiently).
Anyway, there are several calls available from the FragmentManager to do with the backstack. The most useful to me being:
FragmentManager.getBackStackEntryCount()
and
FragmentManager.PopBackStack()
Sorry for late answer, but this might help somebody. I have added functionality like this in my project. I used fragment tabhost to add backstacks within it. Main logic will remain same in others.
Basically I took,
Stack<String> fragmentStack = new Stack<>();
boolean bBackPress = false;
String strPrevTab;
FragmentTabHost tabHost;
strPrevTab = "tag"; // Add tag for tab which is selected by default
tabHost.setOnTabChangedListener( new TabHost.OnTabChangeListener() {
#Override
public void onTabChanged( String tabId ) {
if ( !bBackPress ) {
if ( fragmentStack.contains( tabId ) ) {
fragmentStack.remove( tabId );
}
fragmentStack.push( strPrevTab );
strPrevTab = tabId;
}
}
} );
#Override
public void onBackPressed() {
if ( fragmentStack.size() == 0 ) {
finish();
} else {
strPrevTab = fragmentStack.pop();
bBackPress = true;
tabHost.setCurrentTabByTag( strPrevTab );
bBackPress = false;
}
}
Hope this helps!
Related
I need to display a Fragment with 2 fragments before it being added into back stack. However addToBackStack method belongs to FragmentTransaction so I cannot add all three of them in the single FragmentTransaction cause all three of them will be removed by back button click. But if I use three different FragmentTransations then until the third fragment becomes visible two previous ones become visible to the user too.
Is there a way I can add three Fragments into back stack without making first two of them visible during transaction?
I am not sure you can do that using the native API. Nonetheless you can implement your own stack using the Queue Interface
add an OnBackStackChangedListener to fragmentmanager , then when the last backstack entry is fragment 2 popBackStack two times like this :
0 FragmentHome ( back_stack_name : "fragment_home" )
1 Fragment1( back_stack_name : "fragment_1" )
2 Fragment2 ( back_stack_name : "fragment_2" )
3 Fragment3 ( back_stack_name : "fragment_3" )
code :
note that you should add OnBackStackChangedListener in fragment3 and then after poping backstack remove it from fragmentmanager
// add in fragment3
final FragmentManager fragment_manager = getActivity().getSupportFragmentManager();
fragment_manager
.addOnBackStackChangedListener(new OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if (fragment_manager.getBackStackEntryCount() > 0) {
String last_fragment_name = getLastBackStackFragmentName(fragment_manager);
if (last_fragment_name.equals("fragment_2")) {
fragment_manager.removeOnBackStackChangedListener(this);
fragment_manager.popBackStack();
fragment_manager.popBackStack();
}
}
}
});
private String getLastBackStackFragmentName(FragmentManager fragment_manager ) {
int back_stack_count =fragment_manager.getBackStackEntryCount();
String last_fragment_name = "";
if (back_stack_count>0) {
last_fragment_name = fragment_manager.getBackStackEntryAt(
back_stack_count).getName();
}
return last_fragment_name;
}
So basically what I'm working on is very similar to Instagram application, where there're a number of tabs and users can switch to any tab without any delay no matter what there's anything going on, such as refreshing, reloading, and etc. It also uses back button to go back to the previous preserved tab.
In order to achieve this, I've used FragmentManager with FragmentTransaction to show and hide each fragment which represents each tab. I didn't use replace or attach / detach because they destroy view hierarchy of previous tab.
My implementation works pretty well except that showing and hiding fragments are not committed (I highly doubt that this is a right word to say but so far that's how I understood the flow.), or don't occur immediately when SwipeRefreshLayout is refreshing on the fragment (to be hidden) which was added to FragmentManager later than the one to show.
My implementation follows the rules like these. Let's say we have 4 tabs and my MainActivity is showing the first tab, say FirstFragment, and the user selects the second tab, SecondFragment. Because SecondFragment had never been added before, I add it to FragmentManager by using FragmentTransaction.add and hide FirstFragment by using FragmentTransaction.hide. If the user selects the first tab again, because FirstFragment was previously added to FragmentManager, it doesn't add but only show FirstFragment and just hide SecondFragment. And selecting between these two tabs works smoothly.
But when the user "refreshes" SecondFragment's SwipeRefreshLayout and selects the first tab, FragmentTransaction waits for SecondFragment's refresh to be finished and commits(?) the actual transaction. The strange thing is that the transaction is committed immediately the other way around, from FirstFragment's refresh to SecondFragment.
Because this occurs by the order of addition to FragmentManager, I doubt that the order of addition somehow affects backstack of fragments and there might exists something like UI thread priority so that it forces the fragment transaction to be taken place after later-added fragment's UI transition finishes. But I just don't have enough clues to solve the issue. I've tried attach / detach and backstack thing on FragmentTransaction but couldn't solve the issue. I've tried both FragmentTransaction.commit and FragmentTransaction.commitAllowingStateLoss but neither solved the issue.
These are my MainActivity's sample code.
private ArrayList<Integer> mFragmentsStack; // This is simple psuedo-stack which only stores
// the order of fragments stack to collaborate
// when back button is pressed.
private ArrayList<Fragment> mFragmentsList;
#Override
protected void onCreate() {
mFragmentsStack = new ArrayList<>();
mFragmentsList = new ArrayList<>();
mFragmentsList.add(FirstFragment.newInstance());
mFragmentsList.add(SecondFragment.newInstance());
mFragmentsList.add(ThirdFragment.newInstance());
mFragmentsList.add(FourthFragment.newInstance());
mMainTab = (MainTab) findViewById(R.id.main_tab);
mMainTab.setOnMainTabClickListener(this);
int currentTab = DEFAULT_TAB;
mFragmentsStack.add(currentTab);
getSupportFragmentManager().beginTransaction().add(R.id.main_frame_layout,
mFragmentsList.get(currentTab), String.valueOf(currentTab)).commit();
mMainTab.setCurrentTab(currentTab);
}
// This is custom interface.
#Override
public void onTabClick(int oldPosition, int newPosition) {
if (oldPosition != newPosition) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
// First hide the old tab.
fragmentTransaction.hide(mFragmentsList.get(oldPosition));
// Recalculate the fragment stack.
if (mFragmentsStack.contains(newPosition)) {
mFragmentsStack.remove((Integer) newPosition);
}
mFragmentsStack.add(newPosition);
// Add new fragment if it's not added before, or show new fragment which was already hidden.
Fragment fragment = getSupportFragmentManager().findFragmentByTag(String.valueOf(newPosition));
if (fragment != null) {
fragmentTransaction.show(fragment);
} else {
fragmentTransaction.add(R.id.main_frame_layout, mFragmentsList.get(newPosition),
String.valueOf(newPosition));
}
// Commit the transaction.
fragmentTransaction.commitAllowingStateLoss();
}
}
// It mimics the tab behavior of Instagram Android application.
#Override
public void onBackPressed() {
// If there's only one fragment on stack, super.onBackPressed.
// If it's not, then hide the current fragment and show the previous fragment.
int lastIndexOfFragmentsStack = mFragmentsStack.size() - 1;
if (lastIndexOfFragmentsStack - 1 >= 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.hide(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack)));
fragmentTransaction.show(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack - 1)));
fragmentTransaction.commitAllowingStateLoss();
mMainTab.setCurrentTab(mFragmentsStack.get(lastIndexOfFragmentsStack - 1));
mFragmentsStack.remove(lastIndexOfFragmentsStack);
} else {
super.onBackPressed();
}
}
Just faced the same issue with only difference - I'm switching fragments on toolbar buttons click.
I've managed to get rid of overlapping fragments overriding onHiddenChanged:
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
yourSwipeRefreshLayout.setRefreshing(false);
}
}
Assume I have 4 fragments A B C and D.
A and B are major fragments, C and D are minor.
I use navigation drawer to switch fragments.
A is the default starting fragment.
I want to achieve following features but cannot figure out how to play with the fragment manager and transactions.
A -> B or B -> A, replace current fragment, do not push backstack, but I want to keep the current fragment status (e.g. list view position) after navigate back
A/B -> C/D, add C/D on top of A/B, using back button to navigate back to A/B.
C -> D or D -> C, replace current fagment
C/D -> A/B, remove current fragment C/D and show A/B
Is the only way to implement this function that I should write some complicated function for switching the fragments (and also need to keep what is current fragment and what is the wanted target fragment)?
Is there better way out?
According to #DeeV 's answer, I came out with something like following.
LocalBrowse and WebsiteExplore are main fragments while Settings and About are sub fragments.
It seems to work fine but still a little bit ugly, any better idea?
private void switchToFragment(Class<?> targetFragmentClz) {
if(mCurrentFagment!=null && mCurrentFagment.getClass().equals(targetFragmentClz)) {
return;
}
BaseFragment targetFragment = null;
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(targetFragmentClz.equals(LocalBrowseFragment.class)
|| targetFragmentClz.equals(WebsiteExploreFragment.class)) {
if(mCurrentFagment instanceof SettingsFragment //mCurrentFragment will not be null this time
|| mCurrentFagment instanceof AboutFragment) {
transaction.remove(mCurrentFagment);
}
if(mCurrentMainFagment==null || !mCurrentMainFagment.getClass().equals(targetFragmentClz)) {
targetFragment = (BaseFragment) Fragment.instantiate(this, targetFragmentClz.getName());
targetFragment.setHasOptionsMenu(true);
transaction.replace(R.id.ac_content_frame_main, targetFragment);
mCurrentMainFagment = targetFragment;
}
} else {
targetFragment = (BaseFragment) Fragment.instantiate(this, targetFragmentClz.getName());
targetFragment.setHasOptionsMenu(true);
getSupportFragmentManager().popBackStackImmediate();
transaction.replace(R.id.ac_content_frame_sub, targetFragment)
.addToBackStack(null);
}
transaction.commit();
mCurrentFagment = targetFragment;
}
One method that I can think of is to stack the two types of fragments on each other. So a system like this:
<FrameLayout>
<FrameLayout id="main_container">
<FrameLayout id="sub_container">
<FrameLayout>
Would mean that you have two containers holding fragments. The top one completely covers the other. Thus, you could have two method likes this:
public void swapMainContainer(FragmentManager fm, Fragment frag)
{
fm.beginTransaction().
.replace(R.id.main_container, frag, "TAG")
.commit();
}
public void swapSubContainer(FragmentManager fm, Fragment frag)
{
fm.popBackstackImmediate();
fm.beginTransaction()
.replace(R.id.sub_container, frag, "SUBTAG")
.addToBackStack(null)
.commit();
}
So if you use swapMainContainer() only with Fragment A and Fragment B, they will constantly replace each other but the commits won't be added to the backstack.
If you use swapSubContainer() only with Fragment C and Fragment D, they will likewise replace each other, but "Back" will close them. You are also popping the backstack every time you commit a sub Fragment thus removing the previous commit. Though, if there's nothing in the backstack, it won't do anything.
To remove C/D, simply call popBackStack() and it will remove them from the stack.
The flaw in this approach however is if you have more than these two Fragments that are added to the backstack. It may get corrupted.
EDIT:
Regarding saving view state, the fragment itself will have to handle that via this method.
Currently I have one activity, and fragments are being added to it (search, song details, settings, etc). I implemented side based menu navigation, so now, as a side effect, tehre's no limit to how many Fragments get added to the Backstack. Is there any way I can limit the number of fragments, or remove older entries? Each song details fragment for instance, has a recommended song list, and through that you can go to another song details fragment. It's easily possible to have 30 fragments in the backstack, which if you have DDMS open, you can see the heap size slowly (but surely) increasing.
Edit: One thing I did try to do is if a User clicked one of the side menu options, if that fragment is already in the backstack try to go back to that fragment instead of instantiating a new one, but of course, if a user is on a Song Details page, then he would expect pressing back would take him to that Fragment so that won't work.
Edit 2:
This is my addFragment method (along with Phil's suggestion):
public void addFragment(Fragment fragment) {
FragmentManager fm = getSupportFragmentManager();
if(fm.getBackStackEntryCount() > 2) {
fm.popBackStack();
}
fm.beginTransaction()
.replace(R.id.fragment_container, fragment).addToBackStack("")
.commit();
}
I just tried it, and assuming my Fragment history is: A->B->C->D, going back from D, goes B->A->exit.
I just went 8 levels deep to test: A->B->C->D->E->F->G->H, and going back from H, same thing happened: H->B->A->exit.
All Fragments are getting added through that method above. What I would like to see is: H->G->F->exit.
You can programatically control the number of Fragments in your BackStack:
FragmentManager fm = getActivity().getSupportFragmentManager();
if(fm.getBackStackEntryCount() > 10) {
fm.popBackStack(); // remove one (you can also remove more)
}
Simply check how many Fragments there are in your Backstack and remove if there is an "overflow".
If you want to remove specific Fragments from the BackStack, you will have to implement your own BackStack and override onBackPressed(). Since the Fragment BackStack is a Stack (as the name indicates), only the top element (the last added) can be removed, there is no possibility of removing Fragments in between.
You could for example use
ArrayList<Fragment>
to realize your own stack. Simply add and remove Fragments from that "stack" (it's not really a stack anymore) whenever you desire and handle the loading of previous fragments by overriding the onBackPressed() method.
This is an old question but my answer might help someone.
Why not checking if the fragment is in the stack and pop it? This way you wouldn't have to worry with back stack size (unless you have a lot of fragments).
String backStateName = fragment.getClass().getName();
boolean fragmentPopped = false;
try {
// true if fragment is in the stack
fragmentPopped = fragmentManager.popBackStackImmediate(backStateName, 0);
} catch (Exception e) {
e.printStackTrace();
}
FragmentTransaction ft = fragmentManager.beginTransaction();
if ( !fragmentPopped ) { //fragment not in back stack, add it...
ft.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_FADE );
ft.replace(R.id.main, fragment);
ft.addToBackStack( backStateName );
try {
ft.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
I have an application running a single activity with multiple (2) fragments at a given time. I've got a fragment on the left which functions as a menu for which fragment to
display on the right hand side.
As an example lets say the menu consists of different sports; Football, Basketball, Baseball, Skiing, etc. When the user selects a sport, a fragment with details on the specific sport is displayed to the right.
I've set up my app to display two fragments at once in layout-large and layout-small-landscape. In layout-small-portrait however, only one fragment is displayed at a given time.
Imagine this; a user is browsing the app in layout-small-landscape (two fragments at a time) and selects a sport, Football. Shortly after he selects Basketball. If he now chooses to rotate into layout-small-portrait (one fragment at a time) I want the following to happen:
The Basketball fragment should be visible, but if he presses the back button, he should return to the menu and not to the Football fragment (!) which by default is the previous fragment in the back stack.
I have currently solved this like the following:
....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// static fragments
if(menuFragment == null) menuFragment = new MenuFragment();
if(baseFragment == null) baseFragment = new TimerFragment(); // default content fragment
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
// Determine what layout we're in..
if(app().getLayoutBehavior(this) == LayoutBehavior.SINGLE_FRAGMENT) {
// We are currently in single fragment mode
if(savedInstanceState != null) {
if(!rotateFromSingleToDual) {
// We just changed orientation from dual fragments to single fragment!
// Clear the entire fragment back stack
for(int i=0;i<getSupportFragmentManager().getBackStackEntryCount();i++) {
getSupportFragmentManager().popBackStack();
}
ft.replace(R.id.fragmentOne, menuFragment); // Add menu fragment at the bottom of the stack
ft.replace(R.id.fragmentOne, baseFragment);
ft.addToBackStack(null);
ft.commit();
}
rotateFromSingleToDual = true;
return;
}
rotateFromSingleToDual = true;
ft.replace(R.id.fragmentOne, menuFragment);
} else if(app().getLayoutBehavior(this) == LayoutBehavior.DUAL_FRAGMENTS) {
// We are now in dual fragments mode
if(savedInstanceState != null) {
if(rotateFromSingleToDual) {
// We just changed orientation from single fragment to dual fragments!
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
ft.commit();
}
rotateFromSingleToDual = false;
return;
}
rotateFromSingleToDual = false;
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
}
ft.commit();
}
This works, at least from time to time. However, many times I get java.lang.IllegalStateException: Fragment already added: MenuFragment (....)
Can anyone please give me some pointers as to how to better implement this? My current code is not pretty at all, and I'm sure many developers out there want to achieve exactly this.
Thanks in advance!
A common way to implement this scenario is to only use the fragment stack when in a mode that shows multiple fragments. When you're in the single fragment mode, you start a new activity that's sole job is to display the single fragment and take advantage of the activity back stack.
In your case you'll just need to remember the currently selected spot on rotate to set it as an argument when starting the new activity.
It's explained much better here:-
http://developer.android.com/guide/components/fragments.html
Hope that helps.