I've developed an app, but a user is stating the app is crashing on them in a specific part, the code I suspect to be the culprit can be found below, but to provide you with some context. I have a fragment manager that acts as a wizard, and on the last fragment is a submit button that, when clicked, "processes" the data from all pager fragments and then redirects to a confirmation fragment, all of this happening after the submit button is clicked is done within an Async Task. Also, if it is of any consequence, I am running a progress dialog while the AsyncTask is running.
//Called from Async Task's "onPostExecute()"
private void callback()
{
FragmentManager fragmentManager = getActivity()
.getSupportFragmentManager();
fragmentManager.popBackStack("fraud_report", FragmentManager.POP_BACK_STACK_INCLUSIVE);
Fragment f = new CompletedFragment();
Bundle args = new Bundle();
args.putString("id", reportToSubmit.getReferenceId());
f.setArguments(args);
fragmentManager.beginTransaction()
.replace(R.id.content_frame, f, "completed").commit();
fragmentManager.executePendingTransactions();
}
I have a guess as to what this could be, especially if it is hard to reproduce. You issue is that you are doing a FragmentTransaction after an asynchronous callback, during which any number of things can happen. Therefore you cannot guarantee where you will be in the Activity Lifecycle. The problem is Fragments and State Loss.
http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
https://stackoverflow.com/a/17527246/1856960
Say for example, the user starts the AsyncTask, then ends the Activity, starts another one, or hits the home button. All of this will put the Activity in the background and may trigger a call to onSaveInstanceState. You cannot commit a Fragment Transaction after onSaveInstanceState is called. This is because the FragmentManager and all the state associated with it has already been bundled in onSaveInstanceState, so if the Activity is destroyed and re-created, you will lose your Fragment Transaction.
As for fixing the issue, I wish I had a good suggestion for you. You can look into FragmentTransaction#commitAllowingStateLoss but this should only be used if you are sure the potential downfalls will not negatively impact your application. See the links above for more information.
Related
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!
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.
Update:
I think the leak is coming from getActivity().getSupportLoaderManager().restartLoader(getLoaderId(), null, this);
where i have my object implement LoaderCallback. Is there a way for me to clear the callback i tired setting it to
getActivity().getSupportLoaderManager().restartLoader(getLoaderId(), null, null);
but this crashes
Orig:
I have a list of objects in one of my fragments(A). When I navigate forward I add fragment A to the backstack. After I have navigated to a new fragment and I dump the heap. I still see my object in the heap. When I get the shortest path in the dump it looks like below. I can see that in FragmentManagerImpl there is a reference to fragment A in mActive fragments which is keeping my lists object alive.
Is my fragment supposed to stay in mActive fragments or is this a leak?
Adding to backstack
FragmentTransaction transaction = mFragmentManager.beginTransaction();
updateTransactionWith(info.getReplacement(), transaction, "replace");
transaction.addToBackStack(info.getReplacement().getClass().toString());
transaction.commit();
mFragmentManager.executePendingTransactions();
By calling addToBackStack(), you're requesting the FragmentManager that the Fragment being replaced be just stopped and not destroyed because you're either anticipating that a back button press is very likely or, the Fragment is heavy on initialization and you would still like to avoid doing it again even though the user is not very likely to go back.
The docs clearly state that
If you do not call addToBackStack() when you perform a transaction
that removes a fragment, then that fragment is destroyed when the
transaction is committed and the user cannot navigate back to it.
Whereas, if you do call addToBackStack() when removing a fragment,
then the fragment is stopped and will be resumed if the user navigates
back.
Hence, it's not a memory leak and your observations are quite in line with the expected behaviour.
However, just like an Activity, the system may still choose to destroy this Fragment, if it's running out of memory. But, that's expected behaviour too.
It's not a memory leak. You need to decide how to deal with your fragment's state.
Ideally you implement onSaveInstanceState and onViewStateRestored saving your state to the bundle and restoring it from the bundle respectively.
Alternatively, if you're able to re-create your state easily, you may want to save the bother of (re)storing it using the bundle and just null your references in the onPause method and create them during the onResume method. Be aware that onResume gets called even if the fragment has just been created, so be careful not to do that work more than once.
Either way, be sure to null your references to ensure your objects are marked for GC.
The FragmentManager will decide if it needs to discard and recreate the fragment as necessary in order to allow the user to go back to the fragment you added to the stack. In conditions where there's very little else on the stack and/or there's lots of spare memory it will probably just keep a direct reference to the fragment you added to the back stack.
Given all that, you also need to be careful about keeping references to other fragments, activities, etc as that kind of state is difficult to recreate.
The following approach is recommended for providing proper back navigation:
// Works with either the framework FragmentManager or the
// support package FragmentManager (getSupportFragmentManager).
getSupportFragmentManager().beginTransaction()
.add(detailFragment, "detail")
// Add this transaction to the back stack
.addToBackStack()
.commit();
More info:
http://developer.android.com/guide/components/fragments.html
http://developer.android.com/training/implementing-navigation/temporal.html
API docs: http://developer.android.com/reference/android/app/Fragment.html
I am having a pretty big issue and I am not quite understanding what is happening. I am developing an application that uses Fragments (from the support library) and am using FragmentTransaction.replace() to place new Fragments on to the back stack and replace the old one. The code looks as follows:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = ft.beginTransaction();
// Animations in my res/anim folder
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right);
ft.replace(R.id.fragment_container, newFragment, tag);
ft.addToBackStack(null);
ft.commit();
This is successful in replacing my fragment. My issue is the following. In one Fragment, I have a list of items that is built from user input. Now, when the user clicks next and then clicks the back button (to return to the list), the list is empty because the view is destroyed. Now, I have noted the following:
onSaveInstanceState is not called. I believe this is because that is only called when the parent Activity tells it to. Based on the docs: " 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.". Apparently, performing a replace on the FragmentTransaction is not one of those times. Does anyone have confirmation on this or a better explanation?
setOnRetainInstanceState(true) is not helpful in this situation. Again, I believe this has to do with info from the docs: "Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change)". I am not performing any action in re-creating the activity so this is of no use.
So, I guess my main question is: is there a way to preserve the View state (simply retain the Fragment) when using replace? There is FragmentTransaction.add(), but there are a few issues with this as well. One being that the exit animation is not performed, thus the animation is not correct. Another is that the new Fragment that the old fragment (the one that is being put into a non-visible state) is still clickable. For example, if I have a ListFragment, and I place a content fragment on top of that by using add, I can still click the list items in the ListFragment.
Without being able to see the code of your fragments this is a bit of a guess, but in the past I've run into this same issue and I've found that resetting the adapter in your ListFragment in onViewStateRestored seems to do the trick.
public void onViewStateRestored (Bundle savedInstanceState)
{
super.onViewStateRestored (savedInstanceState);
setListAdapter(new ArrayAdapter(Activity, R.layout.nav_item, objects));
}
Which is weird considering the documentation states that this method is called after onActivityCreated but before onStart. But it seems that it is also called at other times because when the most recent fragment transaction is popped off the back stack this method is called before the previously replaced fragment is displayed. The activity that owns the fragments has not been paused or obscured in any way, so according to the docs onViewStateRestored should not be called since just the fragments were modified. But this seems to work anyway.
It sounds like you simply need to make sure you have properly implemented onCreateView and onDestroyView. The situation you are describing seems to indicate that when the list fragment is put on the back stack (as a result of the replace transaction) Android is calling onDestroyView to free up some resources. However, it apparently has not destroyed the list fragment because when you tap back you are getting back the same instance of the fragment.
Assuming this is all true then, when the user taps back Android will call onCreateView. Any state that you have stored in the fragment's instance variables should still be there and all you need to do is repopulate the view...perhaps set the adapter on the ListView or whatever.
Also make sure your onSaveInstanceState() callback actually does save any instance state that you need to rebuild the view. That way if the fragment actually does get completely destroyed the FragmentManager can restore the state when it needs to recrete the fragment later.
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.