Recently I'm reading the source code of FragmentActivity(sorry that I can't find the source in github, I'm using a native source jar file). The FragmentManager contains the following two members:
ArrayList<Fragment> mAdded; //
ArrayList<Fragment> mActive; //
What's the difference of the two? and in what cases a Fragment will be in mAdded while not in mActive?
mAdded:
Contains fragments that have been added and not removed or detached from the activity.
These fragments are privileged in the sense that they can:
Respond to events such as:
low memory events
configuration changes
Display custom menus and respond to menu item selections.
mActive:
A superset of mAdded that includes all fragments referenced by any FragmentTransaction objects in the backstack.
Fragments that are NOT in mAdded will not be able to respond to events or display custom menus.
What events modify these two fragment lists?
mAdded
a fragment is added to this list if the fragment is added to the activity.
a fragment is removed from this list if:
the fragment is removed from the activity.
the fragment is detached from the activity.
mActive
a fragment is added to this list if the fragment is added to the activity.
a fragment is removed from this list ONLY under the following two scenarios:
it has been removed from the activity and is NOT in the backstack.
a transaction is popped off the backstack and either an add or replace operation is reversed on a fragment that is now no longer referenced by the backstack.
Conclusion
mAdded is a list of fragments that the are alive in a sense, while the mActive list is a complete list of all fragments that are still tied to the activity. mActive contains all living fragments (mAdded) and freeze dried fragments (sitting on the backstack waiting to be resuscitated via backStackRecord.popFromBackStack().
Continuing with the analogy of living and cryogenically preserved entities: as activities execute callbacks like onConfigurationChanged() or onLowMemory(), the only fragments that really care about being passed the opportunity to respond to these events are the live ones.
So you'll see in FragmentManagerImpl that the callback is only looking at the mAdded or living fragments.
fragmentManager.dispatchLowMemory() is called by activity.onLowMemory().
public void dispatchLowMemory() {
if (mAdded != null) {
for (int i=0; i<mAdded.size(); i++) {
Fragment f = mAdded.get(i);
if (f != null) {
f.performLowMemory();
}
}
}
}
I don't know if you're still looking for an answer, but I found some clues about mActive and mAdded.
I found this in the source code of FragmentManager:
public void addFragment(Fragment fragment, boolean moveToStateNow) {
if (mAdded == null) {
mAdded = new ArrayList<Fragment>();
}
if (DEBUG) Log.v(TAG, "add: " + fragment);
makeActive(fragment);
if (!fragment.mDetached) {
if (mAdded.contains(fragment)) {
throw new IllegalStateException("Fragment already added: " + fragment);
}
mAdded.add(fragment);
fragment.mAdded = true;
fragment.mRemoving = false;
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
if (moveToStateNow) {
moveToState(fragment);
}
}
}
The point is around the fifth line we call makeActive(fragment);.
This method calls mActive.add(fragment) so
mActive is incremented each time you call fragmentManager.addFragment()
mAdded is incremented when you call fragmentManager.addFragment() AND when (!fragment.mDetached) is true (look the code above)
So my guess is mAdded contains only the fragments which are attached to the activity and mActive contains the initialized fragments.
But I didn't look deep enough to be sure of my statement ...
I hope this helps
Related
I have an activity running a support viewPager that consists of fragments (the support library variant), which themselves consist of one of three possible fragments. Given that the Android OS destroys and recreates activities on configuration changes (notably screen rotation), I have decided to retain the middle fragments as they run AsyncTasks. The children also may be running other threads so they need to be retained as well. My immediate concern is that:
1) Although the fragments in the viewPager have their onDetach() method called, the children of those fragments never reach onStop(), onDestroy() or onDetach(). Regardless of whether their instances are being retained or not, surely onDetach() should be called since the activity is destroyed.
2) Despite never being stopped, the references to the child fragments are lost; when the activity is recreated and the middle fragments are reattached they fail to find the children using getChildFragmentManager().findFragmentByTag(key).
EDIT - some relevant code from the middle fragments that are run by the viewpager:
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(cachedLocation==null) {
new WaitForLocation().execute(mCallback);
}
else {
swapFragment(LTAG);
}
}
The above is calling swapFragment correctly after recreating, and proves that the middle fragment is retained. However:
public void swapFragment(String key) {
//use passed key to organise displayed fragment in shell
FragmentManager childFragMngr = getChildFragmentManager();
'childFragMngr' does not contain any fragments - mAdded and mActive are both null in the debugger at this point, which is strange because the onStop() and onDetach() methods are never touched for the children.
if(childFragMngr.findFragmentByTag(key)==null) {
Fragment mFragment = null;
if(key.equals(listFragmentTag)) {
//instantiate a list fragment
mFragment = createListFragment();
}
else if(key.equals(detailFragmentTag)) {
//instantiate a detail fragment
mFragment = createDetailFragment();
}
}
if(mFragment!=null){
getChildFragmentManager().beginTransaction().replace(R.id.subfragment_shell, mFragment, key)
.addToBackStack(null)
.commit();
}
}
}
Any help would be greatly appreciated.
First, some background information. I have an Activity that hosts several Fragments; these are hidden and shown such that only one Fragment is visible at a time. Each Fragment hosts several custom Views that I call IncrementCounters. Those views display a number and increment that number by 1 when tapped.
Each of the Fragments has setRetainInstance(true) called on it when it is created in my Activity. When the Activity is created, I check to see if the Fragments exist; if they do, I store a reference to them; if not, I create a new instance, like this:
autonFragment = (AutonomousScoutingFragment) getFragmentManager()
.findFragmentByTag("auton");
teleopFragment = (TeleoperatedScoutingFragment) getFragmentManager()
.findFragmentByTag("teleop");
postMatchFragment = (PostMatchScoutingFragment) getFragmentManager()
.findFragmentByTag("post_match");
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
if (autonFragment == null) {
Log.d("onCreate", "autonFragment is null!");
autonFragment = new AutonomousScoutingFragment();
autonFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, autonFragment, "auton")
.hide(autonFragment);
}
if (teleopFragment == null) {
Log.d("onCreate", "teleopFragment is null!");
teleopFragment = new TeleoperatedScoutingFragment();
teleopFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, teleopFragment, "teleop")
.hide(teleopFragment);
}
if (postMatchFragment == null) {
Log.d("onCreate", "postMatchFragment is null!");
postMatchFragment = new PostMatchScoutingFragment();
postMatchFragment.setRetainInstance(true);
ft.add(R.id.scouting_fragment_container, postMatchFragment,
"post_match").hide(postMatchFragment);
}
ft.commit();
One problem I have is that after every orientation change, it seems as though the Fragments aren't actually being retained, as ever time I see debug prints stating that they are all null. This may be realted to my bigger problem; I'm not sure.
I am trying to figure out how to maintain the value of the number stored in each IncrementCounter across configuration changes, specifically rotation. I have overridden onSaveInstanceState() and onRestoreInstanceState() in IncrementCounter. When I rotate my device, I see that onSaveInstanceState() is called on all of the IncrementCounters I have in my Fragments. However, the corresponding onRestoreInstanceState() is never called, and my IncrementCounters do not have their states restored. What is the proper way to handle something like this? I've been banging my head against my desk for hours about this problem.
Since the Fragments have the setRetainInstance(true), they will not be destroyed on a rotation. The Activity will be destroyed, and the Fragments will be detached until a new Activity is created. However, the Fragments will go through the onCreateView() method again, so you would need to restore their IncrementCounters.
Also, you can save state in onSaveInstanceState(), and then restore it in onCreate(), onCreateView(), and several other methods that are all passed that same bundle as a parameter.
Problems with app:
When orientation changes the app is experiencing these problems:
Both FragmentA and FragmentC now occupy the FrameLayout container.
What works: Everything works as I want it to...prior to rotating the screen.
Activity description in brief:
EditActivity Purpose: edit collection and item fields.
Fragments this activity programmatically creates:
FragmentA - fragment for editing collection fields
FragmentB - ListFragment of items in collection
FragmentC - fragment for editing item fields.
Initial layout: FragmentA sits atop FragmentB, each in their own FrameLayouts.
When user clicks FragmentB's listview item: replace FragmentA with FragmentC to allow user to edit that item's fields. Now FragmentC sits atop FragmentB.
This seems like a very simple notion: the top portion of the activity is for editing either properties of the collection as a whole or a single item from the collection. I don't feel I have done anything wondrous with the layout so I'm a fair bit perplexed that a simple rotation of the phone (emulator) causes these problems that I am having such a dastardly time trying to fix.
Why the Android Fragment Guide example doesn't work for me: their example is much like what I am doing but their detail fragment is either being opened in a new activity or in its own Frame within the current activity, they don't do any swapping of fragments so I cannot glean how they would use the onSaveIstanceState to preserve the fragments that are visible and then use that information in onCreate to recreate the UI that was there prior to orientation change.
EDIT: took out one problem by caving and putting the listfragment in the XML, this solved the perpetual spinning "loading..." problem.
Solved. Oh, the rabbit holes I traveled... At any rate, if you run into problems like this a couple of things to consider:
ultimately I didn't have to write any code in onSaveInstanceState(Bundle outState).
Ultimately I didn't have to make any considerations about handling the backstack in onSaveInstanceState or deal with it the activity's onCreate.
When first "adding" fragments programmatically to the FrameLayout, use replace instead of `add' - this was likely one of the roots of my troubles.
in onCreate check if savedInstanceState's bundle is null, if(savedInstanceState == null), and if it is then I know that the activity hasn't been torn down previously by a configuration change, so here I build fragments that should be displayed right at activity start up. Other fragments that are programmatically brought to life elsewhere (ie, later than the activity's onCreate()), they don't belong in the if, they belong in the else:
else onSaveInstanceState != null and I know there's only one reason this thing's not null, because the system made a bundle named outState in onSaveInstanceState(Bundle outState) and hucked it at the activity's onCreate method where I can now get my grubbies on it. So it is here that I know a couple of things:
for sure the fragments I created in the activity's onCreate are still a part of the activity (I didn't detach or destroy them), but, I cannot make that same claim for the fragments brought to life via a user's actions, those fragments may or may not be currently (at the time of orientation aka configuration change) attached to the activity.
This is a good place for an if-this-thing-is-attached clause. One of things I initially messed up on was I failed to give ALL of my programmatically added fragments a tag; give all programmatically added fragments tags. I can then find out if the savedInstanceState bundle contains that key with savedInstanceState.containsKey(MY_FRAG_TAG) and with getFragmentManager().findFragmentByTag(MY_FRAG_TAG)
So here's the activity's onCreate (simplified):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
// fragments that may not be in the bundle
if(savedInstanceState.containsKey(EDIT_ITEM_TAG)){
editItemFragment = (FragmentC)getFragmentManager().getFragment(savedInstanceState, EDIT_ITEM_TAG);
}
}
// This fragment is NOT programmatically added, ie, it is statically found in an XML file.
// Hence, the system will take care of preserving this fragment on configuration changes.
listFrag = (ListViewFragment)getFragmentManager().findFragmentById(R.id.ListFragment);
// create adapter
adapter = new EditCursorAdapter(this, null);
// set list fragment adapter
listFrag.setListAdapter(adapter);
// prepare the loader
getLoaderManager().initLoader(LOADER_ID, null, this);
}
And the Activity's listener for the list fragment, where FragmentC is swapped for FragmentA:
// listfragment listener
#Override
public void listFragListener(Cursor cursor) {
// checking backstack size
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
// With each listview click there should be only one item in the backstack.
getFragmentManager().popBackStack();
// create new fragment
editItemFragment = FragmentC.newInstance(cursor);
// programmatically add new fragment
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.edit_topFrame, editItemFragment, EDIT_ITEM_TAG);
ft.addToBackStack("pop all of these"); // was testing different ways of popping
ft.commit();
// interesting: this reports the same value as the first log in this method.
// ...clearly addToBackStack(null).commit() doesn't populate the backstack immediately?
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
}
And onSaveInstanceState is naked as a jay bird:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
Summary: I have the activity functioning exactly as I want it to.
Now, if I had a bunch of added fragments then I might handle them in a more programmatic fashion rather than by hard coding the if(savedInstanceState.contains(*hard coded key*). This I tested a little bit but cannot attest to its efficacy, however for someone out there this might spark an idea of what you can do:
Make a private Set of added fragments:
// Collection of Frag Tags
private Set<String> AddedFragmentTagsSet = new HashSet<String>();
In onAttachFragment do something like:
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
// logging which fragments get attached and when
Log.d(TAG, SCOPE +"attached fragment: " +fragment.toString());
// NOTE: XML frags have not frigg'n tags
// add attached fragment's tag to set of tags for attached fragments
AddedFragmentTagsSet.add(fragment.getTag());
// if a fragment has become detached remove its tag from the set
for(String tag : AddedFragmentTagsSet){
if(getFragmentManager().findFragmentByTag(tag).isDetached()){
AddedFragmentTagsSet.remove(tag);
}
Log.d(TAG, SCOPE +"contents of AddedFragmentTagsSet: " +tag);
}
}
Then in the activity's onCreate and within savedInstanceState clauses:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
//////////// find entries that are common to AddedFragmentTagsSet & savedInstanceState's set of keys ///////////
Set<String> commonKeys = savedInstanceState.keySet();
commonKeys.retainAll(AddedFragmentTagsSet);
for(String key : commonKeys){
editItemFragment = FragmentC)getFragmentManager().getFragment(savedInstanceState, key);
}
}
}
...but that is untested and presented merely to spark ideas; in trying to figure out what was wrong with my activity's handling of configuration changes I did stumble and fumble in this direction and think it might bear fruit for the right person; though ultimately, obviously, I found a simpler way to fix my issues this time around.
I am using a ViewPager with 4 pages, and I'm looking for an efficient way to replace/switch between fragments in each page.
This is the interaction pattern that I'm trying to create:
User presses a button on a page that currently holds fragment A
Fragment A is swapped out for some new fragment B
The user does some work in fragment B, and then presses a button when he/she is done
Fragment B is removed, and is replaced by fragment A (the original fragment)
I've found a way to do this, but it seems to have significant flaws. The solution involves removing the original fragment, and then overriding getItemPosition (essentially the method described in this related question):
//An array to keep track of the currently visible fragment in each page
private final Fragment[] activeFragments= new Fragment[4];
public void openFragmentB(ViewPager pager, int position) {
startUpdate(pager);
//Remove the original fragment
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.remove(activeFragments[position]);
transaction.commit();
//Create a new tile search fragment to replace the original fragment
activeFragments[position] = FragmentB.newInstance();
pageStates[position] = PageState.STATE_B;
finishUpdate(pager);
notifyDataSetChanged();
}
#Override
public int getItemPosition(Object object) {
//If the main fragment is not active, return POSITION_NONE
if(object instanceof FragmentA) {
FragmentA a = (FragmentA) object;
if(pageStates[a.getPosition()] != PageState.STATE_A) {
return POSITION_NONE;
}
}
//If the secondary fragment is not active, return POSITION_NONE
if(object instanceof FragmentB) {
FragmentB b = (FragmentB) object;
if(pageStates[b.getPosition()] != PageState.STATE_B) {
return POSITION_NONE;
}
}
return POSITION_UNCHANGED;
}
This method works, but has undesirable side effects. Removing the fragment and setting it's position to POSITION_NONE causes the fragment to be destroyed. So when the user finishes using FragmentB, I would need to create a new instance of FragmentA instead of reusing the original fragment. The main fragments in the pager (FragmentA in this example) will contain relatively large database backed lists, so I want to avoid recreating them if possible.
Essentially I just want to keep references to my 4 main fragments and swap them in and out of pages without having to recreate them every time. Any ideas?
A simple way to avoid recreating your Fragments is to keep them as member variables in your Activity. I do this anyway in conjunction with onRetainCustomNonConfigurationInstance() order to retain my fragments during configuration changes (mostly screen rotation). I keep my Fragments in a 'retainer' object since onRetainCustomNonConfigurationInstance only returns a single object.
In your case, instead of calling Fragment.newInstance() all the time, just check to see if the fragments contained in the retainer object is null before creating a new one. If it isn't null, just re-use the previous instance. This checking should happen in your ViewPager adapter's getItem(int) method.
In effect, doing this basically means you are handling whether or not Fragments are recycled when getItem is called, and overriding the getItemPosition(Object) method to always return POSITION_NONE when for relevant Segments.
FragmentPagerAdapter provides an overrideable method called getItemId that will help you here.
If you assign a unique long value to each Fragment in your collection, and return that in this method, it will force the ViewPager to reload a page when it notices the id has changed.
Better late than never, I hope this helps somebody out there!
In the example on using fragments in the Android docs, when the application is in 'dualview' mode, the details fragment is recreated whenever the application needs to show details for a different title. FragmentTransaction.replace() is used to swap out each old details fragment instance with a new one.
Is this recommended practice? Isn't it wasteful to create a new UI instance when the real intent (no pun intended) is to update what the UI shows, not the UI itself. It seems to me the only reason to create new instances is if one intends to add them to the backstack so the user can retrace steps. Otherwise, is it safe/advisable to update a fragment directly?
In the case of the example, it would mean a method along the lines of DetailsFragment.setShownIndex(). This would be called, passing in the new title index, instead of recreating DetailsFragment.
Suppose we have a version of the example where one activity manages both fragments, but only shows one at a time, swapping each fragment out as needed. Would it be ok for the activity to create an instance of each fragment, retain references to each, and then simply add or remove these two instances from itself as needed?
One possibly sticky consequence of this would be that, when the titles fragment is in resumed state (i.e. in the 'foreground'), selecting a title will result in a call to DetailsFragment.setShownIndex() at a time when the details fragment is in stopped state.
Good idea? Bad idea?
Thanks in advance.
Like you said, the main reason to create new Fragment instances is for ease of using the back stack. It is also perfectly safe to reuse an existing Fragment (looking it up using either FragmentManager.findFragmentById() or FragmentManager.findFragmentByTag()). Sometimes you'll need to make good use of the Fragment methods like isVisible(), isRemoving() etc. so you don't illegally reference UI components when the DetailsFragment is stopped.
Anyway in your proposed single-pane Activity with 2 fragments, your setShownIndex method could set a private field in DetailsFragment which is loaded in onCreateView or onActivityCreated.
e.g.,
DetailsFragment df = getFragmentManager().findFragmentByTag("details");
if (df != null) {
df.setShownIndex(getSelectedIndex());
} else {
df = DetailsFragment.newInstance(getSelectedIndex());
}
fragmentTransaction.replace(R.id.frame, df, "details").commit();
In both cases, whether df is newly created or reused, onCreateView and onActivityCreated will be called when the DetailsFragment gets added to the container.
But if you want a back stack, I highly recommend just creating new instances, otherwise you're just implementing your own back stack for the contents of the DetailsFragment.
I have tried the following code and it works for me :
private void replaceFragment(Class fragmentClass, String FRAGMENT_NAME, android.support.v4.app.FragmentManager fragmentManager) {
Fragment fragment = null;
String backStateName = fragmentClass.getName(); // nome della classe del Fragment
Log.d("Fragment: ", "Creazione Fragment: "+backStateName);
Boolean fragmentExit = isFragmentInBackstack(fragmentManager, backStateName);
if (fragmentExit) { //Il Fragment รจ presente nello stacback
// Fragment exists, go back to that fragment
//// you can also use POP_BACK_STACK_INCLUSIVE flag, depending on flow
fragmentManager.popBackStackImmediate(fragmentClass.getName(), 0);
} else {
// se non esiste lo aggiungiamo
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Inizializzo la transazione del Fragment
android.support.v4.app.FragmentTransaction ft = fragmentManager.beginTransaction();
ft.setCustomAnimations(
R.anim.fragment_slide_left_enter,
R.anim.fragment_slide_left_exit,
R.anim.fragment_slide_right_enter,
R.anim.fragment_slide_right_exit);
ft.replace(R.id.frameLayout_contentMain, fragment, FRAGMENT_NAME);
ft.addToBackStack(fragmentClass.getName());
ft.commit();
// Recupero il numero di Fragment presenti
Integer nFragment = fragmentManager.getBackStackEntryCount();
Log.d("Fragment: ", "Numero di Fragment: "+nFragment);
}
}
To determine if the Fragment is already in StackBack execute this function :
public static boolean isFragmentInBackstack(final android.support.v4.app.FragmentManager fragmentManager, final String fragmentTagName) {
for (int entry = 0; entry < fragmentManager.getBackStackEntryCount(); entry++) {
if (fragmentTagName.equals(fragmentManager.getBackStackEntryAt(entry).getName())) {
return true;
}
}
return false;
}
I Hope I could help you