Fragment which is not top most in backstack is resumed - android

Given the application flow show in the graphic and textually described in the following.
Fragment 1 is the lowest fragment but not in the backstack by setting disallowAddToBackStack.
Fragment 2 is pushed onto the stack, using fragmentTransaction.addToBackStack().
A new instance of fragment 1 is pushed onto the stack.
The top most fragment (fragment 1) is popped from the stack.
Activity 2 becomes foreground.
Activity 1 becomes foreground.
Here is the generalized method I use to handle fragments:
private void changeContainerViewTo(int containerViewId, Fragment fragment,
Activity activity, String backStackTag) {
if (fragmentIsAlreadyPresent(containerViewId, fragment, activity)) { return; }
final FragmentTransaction fragmentTransaction =
activity.getFragmentManager().beginTransaction();
fragmentTransaction.replace(containerViewId, fragment);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
if (backStackTag == null) {
fragmentTransaction.disallowAddToBackStack();
} else {
fragmentTransaction.addToBackStack(backStackTag);
}
fragmentTransaction.commit();
}
Problem
When activity 1 resumes in the last step the lowest instance of fragment 1 also resumes. At this point in time fragment 1 returns null on getActivity().
Question
Why is a fragment which is not the top most on the stack resumed?
If resuming the fragment is correct - how should I handle a detached fragment?

When an Activity is not showing UI and then come to show UI, the FragmentManager associated is dying with all of your fragments and you need to restore its state.
As the documentation says:
There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.
In your Activity onSaveInstanceState and onRestoreInstanceState, try saving you Fragment references and then restore them with something like this:
public void onSaveInstanceState(Bundle outState){
getFragmentManager().putFragment(outState,"myfragment", myfragment);
}
public void onRetoreInstanceState(Bundle inState){
myFragment = getFragmentManager().getFragment(inState, "myfragment");
}
Try this out and have luck! :-)

I don't see how this would happen, unless (based on how you described the steps) you've misunderstood how fragmentTransaction.addToBackStack() works: it manages which transactions are placed in backstack, not fragments.
From the android docs:
By calling addToBackStack(), the replace transaction is saved to the
back stack so the user can reverse the transaction and bring back the
previous fragment by pressing the Back button.
So if your step 2 looked something like this in code:
fragmentTransaction.replace(containerViewId, fragment2);
fragmentTransaction.addToBackStack();
fragmentTransaction.commit();
and your step 3:
fragmentTransaction.disallowAddToBackStack()//or just no call to addToBackStack - you do not say
fragmentTransaction.replace(containerViewId, newfragment1);
fragmentTransaction.commit();
At this point, Fragment2 will be removed from the backstack, and your backstack consists of the two Fragment1 instances. in Step 4 you pop the top one, which means you should have the bottommost Fragment1 now at the top.
This explains why it is the resumed fragment if you return to the activity. But not, i'm afraid, why it is apparently detached from its activity.

Android OS can and will create and destroy fragments when it sees fit. This is likely happening when you launch Activity 2 and return to Activity 1. I'd verify for sure that it isn't the actively displayed fragment. What is probably happening is that you are seeing it do some of the creation steps for fragment 1 before it does the creation steps for fragment 2.
As for handling the detached fragments you should take a look at this page. The gist of it is that you should only be using the getActivity in certain fragment functions(Based on the fragment life cycle). This might mean that you have to move some of your logic to other functions.

Related

Fragment does not call lifecycle methods

I've a fragment A. I add() it with tag like this:
fragmentTransaction.addToBackStack(special_tag);
Then I simply add() fragment B on top of fragment A. After that, I decide to remove fragment B and go back to fragment A using:
activity.fragmentManager.popBackStackImmediate(special_tag, 0)
When I reach the fragment A, it seems that fragment doesn't re-run it's lifecycle methods: onAttach(), onResume(), onCreate() ect.
Can someone explain this behavior and maybe suggest an alternative?
(I need to "refresh" the data when I come back to fragment A second time)
What is causing this result?
Is there a clean solution/work-around?
Update
Fragment B is GuidedStepFragment and does not have a .replace() function. I found that it has finishGuidedStepFragments(), but it behaves the same (it does not call fragment life cycle functions)
Situation (again):
Fragment A (Simple fragment) -> .add(Fragment B) (GuidedStepFragment) -> popBackStackImmediate() or finishGuidedStepFragments()
I add Fragment B like this:
GuidedStepFragment.add(activity.fragmentManager, fragmentB.createInstance())
Using fragmentTransaction.add(Fragment) doesn't remove Fragment A. What is actually happening is that Fragment A is still running behind Fragment B. Since Fragment A never stopped running, it's lifecycle has no need to retrigger.
Consider using fragmentTransaction.replace(Fragment) and replace the fragment in the container (fragment A) with fragment B. If you pop that transaction from the back stack, then Fragment A will reattach and follow your expected lifecycle.
Update
Since you seem to be using GuidedStepFragments from the leanback library, this is a little tricky. GuidedStepFragment actually performs replace(...) under the hood, but you're adding fragment B to a different container so the original behavior I mentioned doesn't apply.
I'm not super familiar with leanback (since it's usually only used for android tv), but I do know that you can at least do the following. If you keep track of your backstack size, when all of the GuidedStepFragments have been popped, you will have returned to your original fragment. For example, let's assume your backstack starts at zero:
activity.fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if (activity.fragmentManager.getBackStackEntryCount() == 0){
// handle your updates
}
}
});
// the next line of code will add an entry to the backstack
GuidedStepFragment.add(activity.fragmentManager, fragmentB.createInstance());
// eventually when back is pressed and the guided fragment is removed, the backstack listener should trigger

What Fragments am I hosting and displaying?

Is there a way to know which Fragment is currently displayed in a given <fragment> container of an Activity without keeping track of all the changes via the onAttachFragment callback?
Is it even possible to know which fragments are displayed when fragment transactions can take place when the user presses the back key? In this latter case, i.e. when a Fragment is re-displayed due to a back, the onAttach is not called.
In my experience, the only way to know for sure which fragment is being displayed is to keep track of that carefully yourself.
For example, you could make a variable in your Activity:
Fragment mCurrentDisplayedFragment;
and then whenever the user requests a different fragment do:
mCurrentFragment = (Fragment) userRequestedFragment;
fragmentManager.replace(container, mCurrentFragment, tag);
Then, whenever you needed to do things to the currently displayed fragment, you could triage it by try/catching a cast or with instanceof.
You could also handle the back pressed behavior by overriding that method in the activity:
#Override
public void onBackPressed() {
int stackSize = fragmentManager.getBackStackEntryCount();
// This counts up from the bottom so the most recent fragment is the biggest number/size
backFragId = fragmentManager.getBackStackEntryAt(stackSize);
// Get a handle on the fragment that is about to be popped
mCurrentFragment = fragmentManager.findFragmentById(backFragId);
super.onBackPressed();
}
Also, are you sure that onAttach is not called when a fragment is popped off the stack? I seem to remember that it will be, and you can call through the interface created there (if you have one and the activity implements it) to register the fragment as the current fragment in the activity at the time.
But to directly answer your question, there isn't a built in way to just know what fragment is currently displayed (and there could be more than one!). The implementation details of that are up to you. Hopefully I've given you some ideas of how it could be handled though. You might also find the FragmentManager documentation helpful.
Each time when you add/replace fragment to the container, use tag for it:
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.container, fragment, tag).commit();
then you can find out the fragment is current visible or not:
Fragment fg = getFragmentManger().findFragmentByTag(tag);
if(fg.isVisible())
//fg is the current visible fragment
Hope this help!

What to do with Fragments when they are dismissed (not visible)?

I am rewriting a Bluetooth app with 3 Activities to use just 1 Activity and 3 Fragments:
So I have now 4 files:
MainActivity.java (contains bluetooth and shared preferences code)
MainFragment.java (contains ellipsis menu to show SettingsFragment)
SettingsFragment.java (contains "scan" button to show ScanningFragment)
ScanningFragment.java (displays nearby bluetooth devices in a list)
It almost works, but as an Android programming newbie I don't understand - what to do with Fragments when I show some other Fragment?
Should I just drop the Fragments (and remove from FragmentManager?) to be garbage collected?
Or should I add these 3 private variables to MainActivity.java and reuse them (when the user navigates forwards and backwards)?
private MainFragment mMainFragment;
private SettingsFragment mSettingsFragment;
private ScanningFragment mScanningFragment;
Or does FragmentManager somehow manage all 3 Fragment for me - regardless if they are visible or not?
Here is my current code (it is simple, I just call replace() all the time)-
public class MainActivity extends Activity implements
MainListener,
SettingsListener,
ScanningListener,
BleWrapperUiCallbacks {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_main); // empty FrameLayout
Fragment fragment = new MainFragment();
getFragmentManager().beginTransaction()
.replace(R.id.root, fragment, "main")
.commit();
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
Fragment fragment = new SettingsFragment();
getFragmentManager().beginTransaction()
.addToBackStack(null)
.replace(R.id.root, fragment, "settings")
.commit();
break;
}
return super.onOptionsItemSelected(item);
}
// implementing SettingsFragment.SettingsListener interface
public void scanClicked() {
// TODO how to stop indicator when returned?
setProgressBarIndeterminateVisibility(true);
String address = // get from shared preferences
Fragment fragment = ScanningFragment.newInstance(address);
getFragmentManager().beginTransaction()
.addToBackStack(null)
.replace(R.id.root, fragment, "scan")
.commit();
}
Should I just drop the Fragments (and remove from FragmentManager?) to
be garbage collected?
No need to do anything else. FragmentManager is the guy in charge of Fragments' lifecycle. Once you call replace(), FragmentManager takes care for the rest. If needed it will keep fragment in memory, or release it.
Or should I add these 3 private variables to MainActivity.java and
reuse them (when the user navigates forwards and backwards)?
No, don't do it because of the said above.
Or does FragmentManager somehow manage all 3 Fragment for me -
regardless if they are visible or not?
Yes, it does. For instance, if you have invisible retained fragment, it's enough to create it once, and FragmentManager will take care of it and will keep it even when activity gets re-created during configuration change.
If you create fragments dynamically (as far as I can see, this is your case) then I suggest to add very first fragment dynamically too. You can do it like this.
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_main); // empty FrameLayout
if (savedInstanceState == null) { // <- important
Fragment fragment = new MainFragment();
getFragmentManager().beginTransaction()
.replace(R.id.root, fragment, "main")
.commit();
}
}
This will ensure you don't duplicate MainFragment on configuration change, because when savedInstanceState is not null, then FragmentManager keeps instance of your fragment already.
Since you are calling .replace() on the fragment manager it's essentially the same thing as calling .remove(). According to the docs:
This is essentially the same as calling remove(Fragment) for all currently
added fragments that were added with the same containerViewId and
then add(int, Fragment, String) with the same arguments given here.
So you don't need to worry about any further management since it will be taken care of for you (and be removed to free up resources). This basically means that when one is shown the other is removed. If you were to call .add() then the fragments would still be alive in the background using up resources but you don't have to worry about that since using .replace() only allows one to live at a time.
If I understand your question correctly, you need not call any method to destroy the fragments after using them. Android OS will take of them. According to the documentation, When you replace the fragment with another, the onStop() method of the fragment will be executed, and documentaed as,
The fragment is not visible. Either the host activity has been stopped or the fragment has been removed from the activity but added to the back stack. A stopped fragment is still alive (all state and member information is retained by the system). However, it is no longer visible to the user and will be killed if the activity is killed.
So the fragment will be killed by the OS when the activity is killed. Till the activity is live, fragment objects will reside in the memory.
EDT:
So, if you want to use the fragment again in future, as document suggests,
Also like an activity, you can retain the state of a fragment using a Bundle, in case the activity's process is killed and you need to restore the fragment state when the activity is recreated. You can save the state during the fragment's onSaveInstanceState() callback and restore it during either onCreate(), onCreateView(), or onActivityCreated(). For more information about saving state, see the Activities document.
Fragment are hard coded in xml can not replaced....
but when you call replace(..) then what happen ??
Ok just consider you have 3 fragment A,B,C . In primary stage A initialize in MainActivity... now you are going to call B from A using replace(....). that means A will go onPause(),onStop() state of lifecycle and B will be initialized and paced in MainActivity...same as for C then B is onPause() ,onStop() state. If you want to reuse A or B then you need to call again A or B from C using replace (..). and then A or B will reinitialize and C goes to onPause(),onStop(). But there is another way you can do this
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
hide(A);
show(B);
ft.commit();
if you use above code block then A still be in the running state of its lifecycle but not visible (just detached from UI ).. so it would be better if you need those fragment again use hide()or show() method because it is less expensive operation then reinitialize the fragment.

How to destroy a Fragment that is on Backstack?

I am trying to implement a method where I want to return to the previous fragment and destroy the current one. However, when I add the fragment to the Backstack it doesn't get destroyed anymore afterwards.
Is there any way to destroy it? Or maybe to return to the previous fragment without using the Backstack?
Edit:
I want to use the backwards navigation as well.
Firstly if you are using the BackStack, it is not typical to need to specifically manually remove Fragments, which suggests you might want to have another think about your design.
That said, to specifically manually remove a Fragment, Override onBackPressed in your Activity which is showing the Fragments, manually remove the Fragment there.
To make it easy to determine which Fragment is currently showing, you can give it a Tag when you show it. For example:
fragTrans.replace(android.R.id.content, myFragment, "MY_FRAGMENT_X");
Then in the onBackPressed function of your Activity
#Override
public void onBackPressed()
{
FragmentManager fragMan = getFragmentManager();
// Check if that Fragment is currently visible
MyFragment myFragment = (MyFragment)fragMan.findFragmentByTag("MY_FRAGMENT_X");
boolean myFragXwasVisible = myFragment.isVisible();
// Let the Activity pop the BackStack as normal
super.onBackPressed();
// If it was your particular Fragment that was visible...
if (myFragXwasVisible)
{
FragmentTransaction trans = fragMan.beginTransaction();
trans.remove(myFragment).commit();
}
}
Note: When it comes to specifically destroying your Fragment object, that is what Java's garbage collection is for. You don't need to worry about that yourself, Java will take care of destroying it when it needs to. That's the whole point.

Activity destroyed by garbage collection and all fragments in backstack are showing instead of only current activity

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

Categories

Resources