Say I've pushed three fragments onto the stack: Fragment A, B, and C.
Goal: When I press back on Fragment C I want to have some hook into determining if Fragment B is showing and call frag.onShow().
Thought process: I added an onBackStackChangedListener in order to determine when fragments were added or removed from the stack. Here I check whether the current fragment is equal to the new one (pushed) or different (popped).
Problem: Both frag and getCurrentFragment() are returning the current fragment AFTER Fragment C has been dismissed. So here the new fragment is Fragment B but getCurrentFragment() is also returning Fragment B so it doesn't think it's a pop.
getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
FragmentManager fm = getSupportFragmentManager();
int count = fm.getBackStackEntryCount();
if (count > 0) {
String name = fm.getBackStackEntryAt(count - 1).getName();
MyFragment frag = (MyFragment) fm.findFragmentByTag(name);
MyFragment currFrag = getCurrentFragment();
if (frag != currFrag) {
if (frag != null) {
frag.onShow();
}
}
}
}
});
public MyFragment getCurrentFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (fragment instanceof MyFragment) {
return (MyFragment) fragment;
}
return null;
}
Question: Is there a way to get the last popped fragment? Or is there a better way to do what I'm trying to do? I can always keep some variable around to determine if I'm popping or pushing but I was hoping for a less hacky feeling way.
Thanks.
Thank you #DeeV for helping me understand fragment visibility.
If you're just stacking Fragments on top of each other, then they are technically all still "visible" and active as far as the FragmentManager is concerned. Assuming that your fragments are only visible at one time, you can call FragmentTransaction#hide(fragment) to hide a Fragment. Fragment#onHiddenChanged() will be called both when you hide it and when you unhide it.
I'll be transferring my code to what they suggested. I also realized that if you do need to do it manually like I was doing earlier, a variable of the last fragment count will help determine whether it was a push or a pop.
Related
I have one activity containing one container that will receive 2 fragments.
Once the activity initialises i start the first fragment with:
showFragment(new FragmentA());
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment, fragment.getTag())
.addToBackStack(fragment.getTag())
.commit();
}
Then when the user clicks on FragmentA, I receive this click on Activity level and I call:
showFragment(new FragmentB());
Now when I press back button it returns to fragment A (thats correct) and when i press again back button it show empty screen (but stays in the same activity). I would like it to just close the app (since the activity has no parent).
There are a lot of posts related with Fragments and backstack, but i can't find a simple solution for this. I would like to avoid the case where I have to check if im doing back press on Fragment A or Fragment B, since i might extend the number of Fragments and I will need to maintain that method.
You are getting blank screen because when you add first fragment you are calling addToBackStack() due to which you are adding a blank screen unknowingly!
now what you can do is call the following method in onBackPressed() and your problem will be solved
public void moveBack()
{
//FM=fragment manager
if (FM != null && FM.getBackStackEntryCount() > 1)
{
FM.popBackStack();
}else {
finish();
}
}
DO NOT CALL SUPER IN ONBACKPRESSED();
just call the above method!
addToBackStack(fragment.getTag())
use it for fragment B only, not for fragment A
I think your fragment A is not popped out correctly, try to use add fragment rather replace to have proper back navigation, however You can check count of back stack using:
FragmentManager fragmentManager = getSupportFragmentManager();
int count = fragmentManager.getBackStackEntryCount();
and you can also directly call finish() in activity onBackPressed() when you want your activity to close on certain fragment count.
I started a new project using tabbed activity of Android Studio. It created an activity with ViewPager and 3 fragments = fragment1, fragment2 and fragment3.
I changed fragment1 to have a listView and a TextView, that I initialize when the fragment is created. The data in the listView is read from an SQL database.
When the application starts, fragment1 is displayed and the listView shows the data read from SQL.
When I swipe from fragment1 to fragment2 and back, fragment1 shows the listview properly. However, when I swipe to fragment3 and back to fragment1 the listview data is lost and I get a blank screen, although the textview shows properly.
When I change the application to have only 2 fragments it does not happen and I can swipe from fragment1 to fragment2 and back many times without any loss of data. The moment I add the 3rd fragment, the data is lost on first swipe to fragment3, but is not lost if I swipe between fragment 1 and fragment2 only.
Any idea why is it happening?
This is happening because by default ViewPager has a function named setOffscreenPageLimit(int i). This tells ViewPager how many of the Fragments need to store in memory. By default it is '2'. So when you swipe to Fragment 2 from Fragment 1 it will still keep Fragment 1 in memory and won't destroy it from memory. But when you swipe to Fragment 3 it will have Fragment 2 and Fragment 3 in memory so it will remove Fragment 1.
That's why when you come back to your first Fragment it have lost its views or data.
Try to use
mViewPager.setOffscreenPageLimit(your total fragment count +1);
Or much better solution
Do not replace your fragment when you swipe in ViewPager. Push it to the backstack and when you comeback to it, Check for the backstack and if fragment is there then just Pop it and display it.
Here is the code:
public void pushFragments(String tag, Fragment fragment) {
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
if (manager.findFragmentByTag(tag) == null) {
ft.add(R.id.frame_container, fragment, tag);
}
String tagOne="com........Frag_one"; //your fragment class name
String tagTwo="com........Frag_two";
String tagThree="com........Frag_three";
String tagFour="com........Frag_profile";
Fragment fragmentOne = manager.findFragmentByTag(tagOne);
Fragment fragmentTwo = manager.findFragmentByTag(tagTwo);
Fragment fragmentThree = manager.findFragmentByTag(tagThree);
Fragment fragmentFour = manager.findFragmentByTag(tagFour);
manager.executePendingTransactions();
// Hide all Fragment
if (fragmentOne != null) {
ft.hide(fragmentOne);
}
if (fragmentTwo != null) {
ft.hide(fragmentTwo);
}
if (fragmentThree != null) {
ft.hide(fragmentThree);
}
if (fragmentFour != null) {
ft.hide(fragmentFour);
}
// Show current Fragment
if (tag.equals(tagOne)) {
if (fragmentOne != null) {
ft.show(fragmentOne);
}
}
if (tag.equals(tagTwo)) {
if (fragmentTwo != null) {
ft.show(fragmentTwo);
}
}
if (tag.equals(tagThree)) {
if (fragmentThree != null) {
ft.show(fragmentThree);
}
}
if (tag.equals(tagFour)) {
if (fragmentFour != null) {
ft.show(fragmentFour);
}
}
ft.commit();
}
And Here is how you call it:
Fragment fragment = new Frag_one();
pushFragments(fragment.getClass().getName(),fragment);
Issue solved. The problem is due to the adapter keeping 3 fragments in memory, as pointed by #Daniel Nugent. The data in the pages is loaded from DB so that when the view is recreated, when fragments are destroyed and recreated as the user swipes, the listview adapter has lost the data, and one has to repopulate it. The problem does not appear on simple examples because usually the data presented is static and is loaded in the onCreateView method.
Given an Activity that acts as a Home page (it never closes) that launches various fragments, how to know when the Activity is visible to the user?
From what I have observed, when I open a fragment the lifecycle for the Activity never changes, onPause() is not called. And when I close the fragment, onResume() is not called on my Activity.
Here is how I am starting my fragments, I am using this method and passing the fragment I want to launch to it.
public void addFragment(int containerId, Fragment fragment, boolean addToBackStack) {
// Check if the fragment has been added already. If so, then
// don't add the fragment.
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getClass().getName());
if(temp != null && temp.isAdded()) {
return;
}
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.add(containerId, fragment, fragment.getClass().getName());
if(addToBackStack)
ft.addToBackStack(null);
ft.commit();
}
What is the methodology for indicating that my Activity is visible again? Thanks in advance!
in the oncreate method of your home activity, call
mFragmentManager.addOnBackStackChangedListener(this) ;
and then define
#Override
public void onBackStackChanged() {
int backStackCount = mFragmentManager.getBackStackEntryCount();
if(backStackCount == 0) {} //back to home screen
}
Your Activity is always Visible even if thousand Fragments are showing at the same time, for the sake of understanding Fragments are just Custom-Views, and the Fragment gives a helping hand in handling your View, so onPause() on your activity does not need to called when a Fragment dies or is born,just like inflating a View.
Just like Sir #Tim Mutton said, you need to check your BackStack to know if you are back, or you can use the ViewGroup method ViewGroup.indexOfChild(View child) - this method will an int of value getChildCount()-1 which means its on top of its fellow sibblings..
Hope it helps
I have a sequence of event via which i have added three fragments to the backstack, one by one. Each of these fragments covers the full screen of the activity.
I have stored the is returned from the commit of Frag1.
Now in Frag3, based on a specific click, I want to go back to Frag1 directly and discard/pop all Fragments in between.
So, when this button is clicked i send a message to the activity which does the following:
getSupportFragmentManager().popBackStack(mFrag1Id, FragmentManager.POP_BACK_STACK_INCLUSIVE);
But i just got a blank screen, so i assume no fragment was loaded.
I even tried:
In commit - fragmentTransaction.addToBackStack("Fragment1");
and then
getSupportFragmentManager().popBackStack("Fragment1", FragmentManager.POP_BACK_STACK_INCLUSIVE);
But it doesn't work.
Could someone please help me with this?
Thanks.
OK so I found the issue.
FragmentManager.POP_BACK_STACK_INCLUSIVE pops all the fragments including the one whose id passed as argument.
SO for example:
getSupportFragmentManager().popBackStack(mFrag1Id, FragmentManager.POP_BACK_STACK_INCLUSIVE);
Here it will pop everything on the stack including fragment whose id id mFrag1Id.
from third fragment you should call popBackStack();
twice (one to remove third fragment and the second to remove second fragment )
android.support.v4.app.FragmentManager fm = getActivity().getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.remove(ThirdFragment.this);
transaction.commit();
fm.popBackStack();
fm.popBackStack();
When you opened Fragment A and you Navigated to Fragment B and then to Fragment C and then You want to close Fragment C and B and land on Fragment A
Now in some scenario, you want to close Fragment C and Fragment B and you want to land on Fragment A... then use this logic of FragmentManager to do such task.
First get the number of fragment entries in back stack (When we are adding any fragment to addToBackStack("Frag1")) at that time fragment back stack entry will increase.
so get using this
FragmentManager fmManager = activity.getSupportFragmentManager();
Log.e("Total Back stack Entry: ", fmManager.getBackStackEntryCount() + "");
Now assume, you want to close current fragment (Fragment C) and your last fragment (Fragment B) so simple logic is getBackStackEntryCount -2 and at that time your back stack entry count will be 3 (Fragment A, Fragment B and Fragment C)
Here -2 is for because we want to go 2 fragment step back (Fragment C
and Fragment B)
So simple two line of Code is:
if (fmManager.getBackStackEntryCount() > 0) {
fmManager.popBackStack(fmManager.getBackStackEntryAt(fmManager.getBackStackEntryCount()-2).getId(), FragmentMaanger.POP_BACK_STACK_INCLUSIVE);
}
You can also do it by adding two time "popBackStack()" and will also work, but it not idle way to do this
FragmentManager fmManager = activity.getSupportFragmentManager();
fmManager.popBackStack();
fmManager.popBackStack();
If you want user to back at the beginning fragment, code snippet below will help you.
public static void popBackStackInclusive(AppCompatActivity activity) {
FragmentManager fragmentManager = activity.getSupportFragmentManager();
for (int i = 1; i < fragmentManager.getBackStackEntryCount(); i++){
try {
int fragmentId = fragmentManager.getBackStackEntryAt(i).getId();
fragmentManager.popBackStack(fragmentId, FragmentManager.POP_BACK_STACK_INCLUSIVE);
} catch (Exception e) {
Timber.d("Fragment Back Stack Error: %s", e.getLocalizedMessage());
}
}
}
Also if you want to prevent user to close app when no fragments at back stack, take a look at below.
#Override
public void onBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
if(fragmentManager.getBackStackEntryCount() > 1) {
super.onBackPressed();
} else {
// TODO: Show dialog if user wants to exit app or;
//finish();
}
}
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.