I have a single Activity application with multiple Fragments that are being switched by using Navigation components. When I switch between two fragments their onCreate() and onDestroy() methods seem to overlap. Thus making it difficult for me to write initialization and clean up code for fragments when they access the same global objects.
Navigating from Framgent_A to Fragment_B has the following order of methods:
Fragment_B.onCreate()
Fragment_A.onDestroy()
In Fragment_A.onDestroy() I reverse the operations I do in Fragment_A.onCreate(). And in Fragment_B I expect things to be in a neutral state when onCreate() is called. However that is not the case since Fragment_A.onDestroy() has not yet been called.
Is the overlap normal on Android or did I configure something wrong in my Navigation components? Is there another way I could achieve what I am trying to do? I know I could couple both Fragments and make it work, but I don't want either Fragment to know about each other. To me it seems weird that Framgnet_A is still alive when Fragment_B is created, when Fragment_B is supposed to replace Fragment_A.
Any help is greatly appreciated!
Edit:
After groing through the source code while debugging I have found out that in FragmentNavigator.navigate() FragmentTransaction.setReorderingAllowed() is called, which allows reordering of operations, even allowing onCreate() of a new fragment to be called before onDestroy() of the previous. The question still remains, how can I solve my problem of correctly cleaning up global state in one Fragment before initializing the same global state in the next Fragment.
The Android Fragment life-cycle is not really an appropriate callback host for your needs. The navigation controller will replace the two fragments with animation, so both are somehow visible the same time and eventually even onPause() of the exiting fragment is called after onResume() of the entering one.
Solution 1: Use OnDestinationChangedListener
The onDestinationChanged() callback is called before any of the life-cycle events. As a very simplified approach (look out for leaks) you could do the following:
findNavController().addOnDestinationChangedListener { _, destination, _ ->
if(shouldCleanupFor(destination)) cleanup()
}
Solution 2: Abstract the global changes away
Instead of having single navigation points change the global state, have a single point of truth for it. This could be another fragment independent of the navigation hierarchy. This then observes the navigation as before:
findNavController(R.id.nav_graph).addOnDestinationChangedListener { _, destination, _ ->
resetAll()
when(distination.id) {
R.id.fragment_a -> prepareForA()
R.id.fragment_b -> prepareForB()
else -> prepareDefault()
}
}
As an additional advantage you could implement the state changes idempotently as well.
Since you have an activity that controls the inflation of your Fragments you can manually control the lifecycles of the fragment that are being inflated. By calling into below methods you can control which fragment is ready to use global data. You will at this point have to, some how pass data back to Mainactivity to establish which fragment is active since your asking about how to inflate 2 fragment simultaneously which will share an object. Better approach would be to have the MainActivity implement FragmentA and FragmentB-detail with specific classes to do Stuff this way you have to treat your app like Tablet and determine 2 pane mode and which point you can use appropriate classes out of those fragments controlled by your Activity. The included link matches what your trying to accomplish
private void addCenterFragments(Fragment fragment) {
try {
removeActiveCenterFragments();
fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.content_fragment, fragment);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
activeCenterFragments.add(fragment);
fragmentTransaction.commit();
}catch (Exception e){
Crashlytics.logException(e);
}
}
private void removeActiveCenterFragments() {
if (activeCenterFragments.size() > 0) {
fragmentTransaction = fragmentManager.beginTransaction();
for (Fragment activeFragment : activeCenterFragments) {
fragmentTransaction.remove(activeFragment);
}
activeCenterFragments.clear();
fragmentTransaction.commit();
}
}
Perhaps you could move some the code related to initialization where you assume a neutral state to that fragments onStart() or onCreateView() method. According to the developer documentation this is where initialization should take place.
Another option available is using an Observer /Observable pattern, where you could notify your Activity once onDestroy() in Fragment A is completed. The Activity would then notify Fragment B that it is safe to assume a cleaned up state and begin initialization.
My case was a little bit different, and I would like to share it in case anyone faced the same issue.
I wanted to do an action in onPause() of the current fragment, but not execute that code when one navigates from a fragment to another. What I had to do was to call isRemoving() method to check if the current fragment is being removed or not. It is set to true when NavController.navigate(...) method is called.
override fun onPause() {
super.onPause()
if (!isRemoving()) {
// Write your code here
}
}
Per Google's Fragment.isRemoving() documentation:
Return true if this fragment is currently being removed from its activity. This is not whether its activity is finishing, but rather whether it is in the process of being removed from its activity.
Related
I am creating a new Android Project and soon android foldable devices will be launched. I have an Activity which has fragment called first fragment.
First Fragment has a button called first button which open second fragment which has a button called second and on click of second, third fragment opens.
Suppose user is in third fragment and user decides to unfold his device, will the user go back to fragment one or will he stay in fragment three. As far as I have understood from the Developer Summit, the activity will be destroyed and recreated when user unfolds his device so technically user goes backs to first fragment leading to poor user experience.
So my question is should I consider even using fragments?, If yes how to manage state so that user goes to the same fragment he was when he folds or unfolds his device.
Following is my code if I am changing fragments
private fun displayView(fragment: Fragment?, title: String) {
if (fragment != null) {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.framelayout_activity_main, fragment, title).commit()
}
}
In onCreate(), you only want to execute a FragmentTransaction if this activity is being
newly created, instead of being recreated from a configuration change. Or, more accurately,
you only want to execute a FragmentTransaction if you do not already have fragments in the state that you want them.
So, a typical approach is to see if you already have a fragment in your container:
override fun onCreate(state: Bundle) {
super.onCreate(state)
if (supportFragmentManager.findFragmentById(R.id.framelayout_activity_main) == null) {
// do something to show your fragment
}
// other good stuff goes here
}
On the first onCreate() invocation, findFragmentById() will return null, so you execute your code to display your first fragment. On a subsequent onCreate() invocation after a configuration change, Android will have already set up your fragment(s) for you by the time onCreate() is called. So, in that case, findFragmentById() will return something other than null, so you know that you already have a fragment in your container and do not need to do anything more.
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
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.
Here is my code
public void changeFragment(Fragment contentFragment, Fragment lastFragment, int resourseID,int animationType, boolean addToStack) {
if (contentFragment != lastFragment) {
mContentFragmentTransaction = getFragmentManager().beginTransaction();
FragmentTransactionExtended fragmentTransactionExtended = new FragmentTransactionExtended(this, mContentFragmentTransaction,lastFragment,contentFragment, resourseID);
fragmentTransactionExtended.addTransition(animationType);
mContentFragmentTransaction.replace(resourseID, contentFragment);
if (addToStack) {
mContentFragmentTransaction.addToBackStack(null);
}
mContentFragmentTransaction.commit();
}
}
The problem is, everytime I changeFragment using this method, the fragment's oncreate will fire multiple times, first time, it runs once, second time twice , third time three times, hope someone know what's happening here.
addToBackstack creates a snapshot of your fragments state. Which means when you press the back button, you are actually reverting to the last state that addToBackstack was called on.
You have to call replace on the container that has the fragment, not the fragment itself. So instead of calling replace(R.id.dashboard, fragment) it should be replace(R.id.dashboards_container, fragment).
have you tried to use an other method, like remove(), then do an add(). or anything similar? some people say's replace() method does not always behave correctly.
If the state of the activity has already been saved its no longer safe to call commit. You must call commitAllowingStateLoss() instead.
Even if you use replace or remove calls you can't do this. Only work around I have found is to create a new instance of a fragment and add it every time. Once you remove or replace a fragment it is best to drop all of your references to it so the GC can take care of it.
regards maven
I'm interested in the best way to have a single activity that switches between two fragments.
I've read probably 15 Stack Overflow posts and 5 blogs posts on how to do this, and, while I think I cobbled together a solution, I'm not convinced it's the best one. So, I want to hear people's opinions on the right way to handle this, especially with regards to the lifecycle of the parent activity and the fragments.
Here is the situation in detail:
A parent activity that can display one of two possible fragments.
The two fragments have state that I would like to persist across a session, but does not necessarily need to be persisted between sessions.
A number of other activities, such that the parent activity and the fragments could get buried in the back stack and destroyed due to low memory.
I want the ability to use the back button to move between the fragments (So as I understand it, I can't use setRetainInstance).
In addition to general architecture advice, I have the following outstanding questions:
If the parent activity is destroyed due to low memory, how do I guarantee that the states of both fragments will be retained, as per this post: When a Fragment is replaced and put in the back stack (or removed) does it stay in memory?. Do I just need a pointer to each fragment in the parent activity?
What is the best way for the parent activity to keep track of which fragment it is currently displaying?
Thanks in advance!
I ended up adding both of the fragments using the support fragment manager and then using detach/attach to switch between them. I was able to use commitAllowingStateLoss() because I retain the state of the view elsewhere, and manually set the correct fragment in onResume().
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.my_layout, new AFragment(), TAG_A);
fragmentTransaction.add(R.id.my_layout, new BFragment(), TAG_B);
fragmentTransaction.commit();
}
public void onResume() {
super.onResume();
if (this.shouldShowA) {
switchToA();
} else {
switchToB();
}
}
private void switchToA() {
AFragment fragA = (AFragment) getSupportFragmentManager().findFragmentByTag(TAG_A);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.detach(getSupportFragmentManager().findFragmentByTag(TAG_B));
fragmentTransaction.attach(fragA);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
}
You might want to consider using a ViewPager in your parent Activity so you can switch between the Fragments.
So you would be able to swipe through them.
if you want to persist their state during a session even if the parent activity is destroyed, you need to make them Parcelable, so you can save the state even if the class isn't instantiated at that time. You also need to do this if your rotating your device and want to keep the current situation/data on the Screen.
You should write them to a Parcelable in their onPause methods and recreate them from it in the onResume one. This way it doesn't matter if they are destroyed or have to be recreated due to changes in the devices orientation.
if you want to be able to switch between those fragments with the Backbutton, you can catch the buttonClick for onBackPressed and handle it accordingly.
If you need to figure out what Fragment your displaying at a given time you ask your ViewPager what Fragment he is displaying at that time, so you don't have to keep track, you can just ask someone who knows, if you need to know it.