In my app, I have the following architecture:
MainActivity
|_ FirstFragment
| |_ GridFragment
| |_ MapFragment
|
|_ SecondFragment
|
|_ ThirdFragment
Please note that:
MainActivity is using ActionBarSherlock and SlidingMenu.
SlidingMenu switches between fragments (FirstFragment, SecondFragment, ThirdFragment).
FirstFragment creates a button into the action bar to switch between grid and map mode. This is, to switch between its subfragments.
What I want to achieve
I need fragments need to preserve its state when switching between them. This is, if I am in FirstFragment and then select map mode (show MapFragment subfragment), and from the slide menu choose another option and come back, I should see the map again. Right now, it's resetting each fragment when selecting its section from the slide menu.
This isn't surprising, as at the moment I am commiting transactions with new FirstFragment() when the menu options are pressed.
However, I first tried to have references to each fragment into his parent. For example, MainActivity had three fragments members which, when commiting transactions, were checked if exist previously and instantiated if necessary. This worked fine until I added the second level of fragments, then it started throwing exceptions when committing the transaction (saying that the Activity was destroyed).
As you may have noticed, I am far from being an Android expert and need some guidance on this topic.
How can I preserve fragment states without having a reference for each of them?
Next thing I tried after posting the question, was to avoid keeping the fragments in local variables inside its container and using FragmentManager.getFragmentByTag() to access them at any given time, but the problem persisted, as FragmentTransaction.replace() was destroying the fragments.
My solution
As Luksprog pointed out in his comment, I had to manage all fragments manually. In order to achieve this, I had to go back to my former approach, where I had local variables for each fragment. Then, MainActivity does the following:
Instantiate its three fragments.
this.firstFragment = new FirstFragment();
this.secondFragment = new SecondFragment();
this.thirdFragment = new ThirdFragment();
Attach the three fragments, and hide all of them except the initial section.
getSupportFragmentManager()
.beginTransaction()
.add(R.id.content_frame, this.firstFragment)
.add(R.id.content_frame, this.secondFragment)
.add(R.id.content_frame, this.thirdFragment)
.hide(this.secondFragment)
.hide(this.thirdFragment)
.commit();
To switch content, SlideMenu is calling this function:
public void switchContent(Fragment newContent) {
if (newContent != null) {
getSupportFragmentManager()
.beginTransaction()
.detach(this.firstFragment)
.detach(this.secondFragment)
.detach(this.thirdFragment)
.attach(newContent)
.commit();
// Restore menu open gesture if map is not present
if (!(newContent instanceof firstFragment) && getSlidingMenu().getTouchModeAbove() != SlidingMenu.TOUCHMODE_FULLSCREEN)
getSlidingMenu().setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
// Set menu open gesture if map is present
if (newContent instanceof firstFragment && firstFragment.currentFragment == FirstFragment.MAP_FRAGMENT)
getSlidingMenu().setTouchModeAbove(SlidingMenu.TOUCHMODE_NONE);
getSlidingMenu().showContent();
}
}
Then, the fragments which has subfragments (FirstFragment) is doing the same but:
It's using getChildFragmentManager() instead of getSupportFragmentManager().
It's using show() and hide() to replace content, because when using detach() and attach() the map wasn't preserving it's coordinates.
Related
I've done some research but I really couldn't find the answer.
I have single activity with side menu, and holder. I have many (non support) fragments, and all of them are using same holder (one at a time).
When user uses menu (it's in main activity), and goes another page, I want to add name of the current fragment to backstack (using .addToBackStack(Fragment1.class.getName())), but I couldn't find how to get current fragment.
I don't want to implement interface etc to keep track of current fragment. There is a super simple way using fragmentManger isn't there?
You can get your current fragment like this:
if (getFragmentManager().getBackStackEntryCount() > 1) {
Fragment f = getFragmentManager().findFragmentById(R.id.content_frame);
if (f instanceof BlankFragment) {
// Do something
}
}
OK,
If you want to get latest entry from backstack(thanks to #AndroidGeek);
fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount()-1);
and, if you want to get currently active fragment (thanks to #Salman500 #AndroidGeek);
Fragment f = getFragmentManager().findFragmentById(R.id.fragment_holder);
you can use this to get fragment id for non support fragment
Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_id);
if(fragment!=null)
{
getFragmentManager()
.beginTransaction()
.addToBackStack(null)
.commit();
}
You can keep track of fragments in the main activity (with variables) and access them there. Example:
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction= manager.beginTransaction();
MyFragment myFragment = new MyFragment();
myFragment.doSomething();
Adding to the back-stack:
FragmentTransaction fragment = getSupportFragmentManager().beginTransaction();
fragment.addToBackStack(fragment);
fragment.commit();
This is answered here: get currently displayed fragment
Just use addToBackStack() before you commit() the fragment trancsaction. See it here
So your code will look like :
...
fragmentTransaction.replace(R.id.holder, newFragmentToShow, newFragmentTag);
fragmentTransaction.addToBackStack();
fragmentTransaction.commit();
...
EDIT : after OP was edited
You do not need the fragment class to call addToBackStack() as you have mentioned in the OP. The String argument is an optional string just to keep the state for the backstack state. You should see the documentation
It is all internally managed and the current active fragment is automatically added to the backStack, you may call it from where ever you want, it will always use current active fragment.
I write this question because I would like to know which is the best way to manage this context: I have a MainActivity with Navigation Drawer and whenever I select an item in Navigation Drawer, I create a new fragment and through the FragmentTransaction I replace the previous fragment with the new one.
Now, in every fragment I have an AsyncTask that performs some task (eg download data from web or perform a query on a local sqlite database).
My question is: how can I avoid to recreate every time the fragment and restart AsyncTask when I press an element in Navigation Drawer? Which is the best way to manage this situation?
This is the method that I use in MainActivity to display fragment when I press an item in Navigation Drawer:
private void displayView(int index) {
Fragment f = null;
switch(index) {
case 1:
f = Fragment1.newInstance();
break;
case 2:
f = Fragment2.newInstance();
break;
}
if(f != null) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.container, f);
ft.commit();
}
}
Thanks in advance!
Hiding a fragment is an alternative to removing it. Removal (if not added to the backstack) causes the fragment to be completely torn down (pause, stop, destroyView, destroy, detach). Hiding doesn't change the fragment's lifecycle state, it just makes it not visible. The FragmentTransaction methods are hide() and show(). Initially, you would add both your fragments, and then hide the unselected one. When a fragment is selected from the navigation drawer, if it is not already visible, you would hide the old selection and make the new selection visible. Something like this:
getSupportFragmentManager()
.beginTransaction()
.hide(oldSelectedFrag)
.show(newSelectedFrag)
.commit();
The visible/hidden status of a fragment is available using isHidden(). There is also a Fragment callback: onHiddenChanged(). Keep in mind that when a fragment is hidden it is still active in the sense of being in the started or resumed state. You may want to use the hidden status to disable refreshes or other actions that are only needed when it is visible. If the fragment has an option menu, you may also want disable that using setMenuVisibility() when the fragment is hidden. I don't think the FragmentManager does that for you automatically.
I have a problem that I've been dealing with for the last couple o days and don't seem to find an answer to it.
Description : I have a main activity which contains a navigation drawer. Each item of the navigation drawer (when clicked) creates a fragment. In that fragment, there is a listView of objects, which creates other fragments when clicked. In those fragments i have another listView of objects which opens other fragments. In other words, there series of fragment that open other fragment. Something like this:
http://s22.postimg.org/pddo5gsv5/backstack.png
In order to be able to get back to each fragment, I've implemented the addToBackstack("string") method.
My question is, how can I implement correct backstack for my application so that when i click a navigation Drawer item, all the fragments that have been added to backstack are cleared, without the one that the navigation Drawer item opens.
Any help would be appreciated. Thank you !
EDIT
Ok, it seems I managed to figure it out. Considering what advices i received from the replies, here's the solution I came up with:
#Override
public void onBackPressed() {
int count = getFragmentManager().getBackStackEntryCount();
if (count != 0) {
FragmentManager.BackStackEntry backEntry = getFragmentManager()
.getBackStackEntryAt(
getFragmentManager().getBackStackEntryCount() - 1);
if (backEntry.getName() == NAVIGATION) {
finish();
} else
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
To put it in words: First, i added a backstack entry even for the top level fragments, given them a specific tag. The I have overridden the Activity's back button function so that when the last backstack entry is a top-level fragment to finish the activity (so that it not simply detach the fragment from activity, living it empty). Otherwise, if the last entry isn't an top-level fragment, execute a popBackStack.
PS: All non-top-level fragments are added to the backstack with a different tag then the top-level one. Also, i had to do a POP_BACK_STACK_INCLUSIVE in the navigation Drawer's click listener.
getFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
Thank you all for the advices and hopefully this EDIT help other users.
You can use the following code to solve your problem:
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, fragment)
.addToBackStack(fragment_tag)
.commit();
In order to make the code above work, you have to create the fragments dynamically. As hardcoded fragments cannot be replaced. To do that, you can create a container (FrameLayout etc.) which in our example has the id fragment_container. Then, the code above will add the fragment in the container dynamically. Finally, you have to pass as parameter in the addToBackStack method the fragment_tag. That means, that this transaction will be added in the back stack. And finally, in order to get it from the backstack you have to use the code below:
getFragmentManager().popBackStack(fragment_tag, FragmentManager.POP_BACK_STACK_INCLUSIVE));
The POP_BACK_STACK_INCLUSIVE flag, insures that "all matching entries will be consumed until one that doesn't match is found or the bottom of the stack is reached. Otherwise, all entries up to but not including that entry will be removed."
You can clear the fragment backstack by using something like:
fragmentManager.popBackStack("string", FragmentManager.POP_BACK_STACK_INCLUSIVE);
and then you can addToBackstack and commit as usual. More info.
A code snippet that shows the way I normally use it in navigation drawers:
FragmentManager fragmentManager = getSupportFragmentManager();
if(clearBackStack) {
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
if(!clearBackStack) {
ft.addToBackStack(null);
}
ft.commit();
So I have enabled the setting to destroy actvities when you navigate away from an activity
Settings=>Developer Options=>Don't Keep activites
This should basically replicate an activity or fragment getting garbaged collected and then I have to restore the data via the bundle savedinstancestate.
So I understand how that works. But it seems when I navigate from fragment 1 to fragment 2 and then put the application in the background and then in the foreground(destroying the activity)
Both fragment 1 and fragment 2 show at the same time. In which only fragment 2 should be showing.
I do not know if this is something standard that I have to manage hiding and showing fragments onsavedinstance. Or if something in my code is breaking things. Below is how I push fragments which I hope is helpful:
public void pushFragmentWithAnimation(FragmentManager fm, int parentId, Fragment currentFrag, Fragment newFrag, int animEntry, int animExit) {
hideSoftKeyboard(currentFrag.getActivity());
FragmentTransaction ft = fm.beginTransaction();
// See: http://developer.android.com/reference/android/app/FragmentTransaction.html#setCustomAnimations(int, int, int, int)
ft.setCustomAnimations(animEntry, animExit, animEntry, animExit);
ft.add(parentId, newFrag, String.format("Entry%d", fm.getBackStackEntryCount())).hide(currentFrag).show(newFrag);
ft.addToBackStack(null);
ft.commit();
}
Fragment 1 is still in the backstack because when I press back I only see fragment 1. Let me know if you know why this is happening.
The lifecycle of XML added Fragments and programmatically added Fragments differ enough to make mixing them a bad idea, as explained in detail here.
The easiest way around this is to make all fragments programmatically added by replacing your XML inflated Fragment with a FrameLayout of the same ID, then in your onCreate add
FragmentManager fragMgr = getSupportFragmentManager();
if (null == fragMgr.findFragmentByTag(FRAG_TAG))
{
fragMgr.beginTransaction().
add(R.id.fragment, new Fragment1(), FRAG_TAG).commit();
}
Where FRAG_TAG is any unique string. This ensures that Fragment1 is only created if it is not already in the layout.
I am not entirely sure why this solution works. I assume its related to if the activity gets killed that it does not keep track of which fragment is currently shown and shows all of the fragments. So basically I needed to replace:
ft.add(parentId, newFrag, String.format("Entry%d", fm.getBackStackEntryCount())).hide(currentFrag).show(newFrag);
with
ft.replace(parentId, newFrag, tag);
Then when I create the initial fragment in the main activity. I only would do that when
if(savedInstanceState==null){
My updated code is below: https://github.com/CorradoDev/FragmentTest/tree/2c53f9f42e835da768f61b0233f3ab5b3adf2448
I created a sample app to test this overlapping issue.
I have a fragment type, Fragment1, and I create a new instance of Fragment1 and add it to a FrameLayout in my activity at runtime. I add the fragment with the help of a few buttons.
Note: I have given each new instance of Fragment1 a different number(#1, #2, #3, etc.) to display on the UI to help me figure out which fragment I am viewing.
So.. here is what I do:
Click on Button 3, create new instance of Fragment1 and add it to Frame1.
Click on Button 4, create new instance of Fragment1 and add it to Frame1 and add it to the fragment backstack.
Repeat 1 and 2.
Repeat 1 and 2.
Now, I have fragments in this order: 1(#1),2(#2),1(#3),2(#4),1(#5),2(#6).
I press the back key when viewing fragment #6.
Back key press, UI displays (#5).
Back key press, UI displays (#3 AND #5),
Back key press, UI displays (#1, #3, AND #5)
It seems fragments are getting displayed ON TOP of each other.
WHY? Is there an overlapping issue? How can I clear out this overlapping issue. I thought this would be an issue on the compatibility library... but it is also on 3.0.
Code for adding fragments:
public int doFragmentChange(int cont1, Fragment frag1, String tag1, int cont2, Fragment frag2, String tag2,
boolean addToStack, String stackTag) {
FragmentManager fm = getFragmentManager();// getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
if (frag1 != null) {
ft.replace(cont1, frag1, tag1);
}
if (frag2 != null) {
ft.replace(cont2, frag2, tag2);
}
// add fragment to stack
if (addToStack)
ft.addToBackStack(stackTag);
return ft.commit();
}
If you perform two add calls one after the other (two commit calls) then yes the fragments will appear overlaid, one on top of the other effectively.
So (for new example) if say you replace frag1 with frag2 and then frag3 with frag4 in the same frame with no backstack transaction then I would expect frag2 and frag4 to be overlaid.
Furtheremore there is also a potential issue in your chaining of replace. You should call a separate commit for each. See Android — Replace Fragment Back Stack With New Stack?.
Just override the onBackPress() or onKeyUp and remove the top fragment.