note: This is a lengthy and probably boring post
Your problem with the ViewPager is not new. Don't feel bad. We've all went through what appeared to be a magic solution to a new paradigm (and I will put new in Italics because it wasn't necessarily new at the time). With the added support for Fragments (through the State and Stateless adapters that you mention) things quickly became more interesting.
I'm not going to go into the details whether the adapters are good or annoying (you can draw your own conclusions) or why the adapters (and the widget itself) lack very basic stuff that nobody understands what where the Android developers thinking when they exposed the public API for these.
Instead, I'm going to help you manage Activity/Fragment communication the way -I think- everybody does.
What is happening with Fragments in a ViewPager?
The Activity/Fragment concept is -in my humble opinion- horrible and it was a hack. But an effective one, since it quickly proved that it worked, to the point where the Android team decided to start adding Fragment support to more and more stuff. Even nested Fragments, yes Fragments inside Fragments! And since Android developers (less and less but still very often) have to deal with old versions of the platform, then all this support was initially added to the Support Library, and it never moved out of it.
But let's get back on topic. The Fragment lifecycle is somewhat confusing (in part thanks to the poor naming of some lifecycle methods and the order they are -not- guaranteed to occur). Some things are obscurely hidden behind unfriendly callbacks (TreeLayout anyone?).
So… the ViewPager needs an adapter. The adapter is in charge of supplying the ViewPager with its views. So while the ViewPager is a View that understands what a touch, drag, fling, draw, measure, etc. is, it really expects an Adapter to provide the data to display. (This is a huge simplification).
And here we have Two types of adapters which know how to deal with Fragments. One maintains a state and the other doesn't. Meaning one doesn't really release anything (FragmentPagerAdapter) and one does indeed release its fragments (FragmentStatePagerAdapter)… but wait… what is "release" in this context?
Turns out that Fragments don't really exist in a free world, they are dominated by a FragmentManager, who makes sure that they are not late and decides when to release them. This is a rule that not even the Adapters can override! So they must report to this FragmentManager anyway. You can tell that the adapter must talk to this FragmentManager because it must receive one in the Constructor!
public static class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
Who is this FragmentManager anyway?
Well, the documentation is not very friendly, it just says:
Interface for interacting with Fragment objects inside of an Activity
Ok, thank you Mr. obvious!
In practice, the FragmentManager is a static class, you don't create it, you just get it from an Activity.
Now think of the FragmentManager of a controller who keeps a reference to your fragments, even long after you've left them. It knows how to bring them back, it helps them save their state when configuration changes (rotations, for example), it keeps a pile/stack of them, knowing in which order they can be popped back to life and it does all this, sporting FragmentTransactions that must be committed, much like a relational database transaction.
Most of this complexity (and I'm sure they had their reasons) happens behind the scenes, so you don't really have to worry too much about it.
Can we go back to the original question now?
Yes yes… it's a long subject as you can see, but let's get to the point, because I have to go back to work…
When the Fragment gets out of view, the adapter secretly tells the FragmentManager: "Yo dawg, this Fragment is no longer needed here -for now-". (It may not use that phrase but something like that). For a more detailed response, you can actually look at the more or less recent code of the FragmentStatePagerAdapter and learn a lot from it.
Look how it has a list of Fragments and SavedStates:
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
and a reference to a FragmentManager:
private final FragmentManager mFragmentManager;
Not surprisingly, FragmentPagerAdapter doesn't have a list of Fragments nor a list of saved States, it simply lets the FragmentManager do its job.
So let's first look at the "State" pager adapter to see what's doing when it's time to Destroy a Fragment…
With Google's permission, let's look at the source code for destroyItem()
:
1 @Override
2 public void destroyItem(ViewGroup container, int position, Object object) {
3 Fragment fragment = (Fragment)object;
4 if (mCurTransaction == null) {
5 mCurTransaction = mFragmentManager.beginTransaction();
6 }
7 if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
8 + " v=" + ((Fragment)object).getView());
9 while (mSavedState.size() <= position) {
10 mSavedState.add(null);
11 }
12 mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
13 mFragments.set(position, null);
14 mCurTransaction.remove(fragment);
15 }
Line 4 starts a FragmentTransaction if one hasn't already been started.
Lines 9-11 pads out the mSavedState array with null entries until it’s at least the size of the index of the fragment we’re removing.
Line 12 saves the state of the Fragment being removed (so it can be restored in the future if needed).
Line 13 is effectively removing the Fragment reference…
Line 14 adds this to the Fragment's Manager transaction so the FragmentManager knows what to do.
(Note: there's an obscure bug in the ViewPager when you add/change/remove Fragments dynamically, I'll link to the problem/solution at the end).
When you add a Fragment to the ViewPager, the process is relatively similar (see instantiateItem()
method…
It first checks if the object is already instantiated, then it returns it immediately.
If the Fragment is not there, one is created…
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
Remember you extend this Adapter and create your own getItem()
method, this is where it gets called. You are giving the Adapter a newly created Fragment.
Next the Adapter checks for the savedState of the fragment to see if it can find one (and here it makes a mistake) (see link at the end)…
Finally it proceeds to add the newly received Fragment:
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
It must add null padding for the array to be the exact size, and also uses the FragmentTransaction of course.
The moral of the story so far is that the Adapter keeps its own collection of stuff but it keeps the boss (a.k.a.: FragmentManager) happy by letting him know he's in control.
For reference, the support v13 versions are pretty much the same, but have references to the non support version of Fragment, FragmentManager, FragmentTransaction, etc.
So if the Adapters either keep a list and ask the FragmentManager (through a FragmentTransaction) or just use the FragmentManager, what does the FragmentManger do?!
This is lot more "complicated" but the FragmentManager implementation has a list of Added and Active fragments (along with a myriad of other collections and data structures).
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
^ That's coming from the FragmentManagerImpl class!
So I won't go into details about the FragmentManager (you can find its source code here), because it's a really big class and it uses transactions for everything and it's super messy. It keeps a state machine about each fragment (created, initializing, etc.). The interesting method is perhaps moveToState()
and this is where the Fragment lifecycle callbacks are made, so take a look at the source code to see what's going on.
Also take a look at the removeFragment()
method there, which ends up calling the moveToState()
method in the end.
Enough with all this… when can I call getActivity() and not get null in my Fragments?
Ok, so you provided an example of what you wanted to do.
You have ViewPager with 5 Fragments (for example) and you want to notify them that something has happened so they can do something about it. A lot of somethings.
I'd say that this is a typical Observer pattern + ViewPager.OnPageChangeListener().
The observer pattern is simple, you create an Interface like:
public interface FragmentInterface {
void onBecameVisible();
}
Your Fragments implement this interface…
public Fragment YourFragment implements FragmentInterface {
and then you have a:
@Override
public void onBecameVisible() {
if (getActivity() != null && !getActivity().isFinishing()) {
// do something with your Activity -which should also implement an interface!-
}
}
And who calls onBecameVisible?
Your ViewPager.OnPageChangeListener() in your Activity:
public void onPageSelected(final int i) {
FragmentInterface fragment = (FragmentInterface) mPagerAdapter.instantiateItem(mViewPager, i);
if (fragment != null) {
fragment.onBecameVisible();
}
}
So now the Activity can reliably tell the Fragment that it has become visible.
Now… if you want to tell the OTHER Fragments, then you have to have a list of "listeners" (Observer pattern) but you must understand that according to what we've seen about the Adapters and the Fragment Manager, Fragments may be detached (even tho they may not necessarily be destroyed). The fact that they are not visible, could mean they are prone to have their view hierarchy destroyed (so changing it at that point would not be possible).
Your best bet is to make sure your onCreateView is capable of asking which view should be displayed based upon your business logic.
The best way to understand the lifecycle is to add a Log to EACH of the Fragment lifecycle (onStart, Stop, Pause, Resume, Detach, Destroy, ActivityCreated, etc…) and see how they react when you move through the ViewPager.
I hope this lengthy post gives you an idea about what's the ViewPager doing with your Fragments. Let us know if you have more specific questions or if I haven't really helped you :)
Regarding the BUG I mentioned earlier, take a look at this post which talks about a bug in (and a fix for) the way FragmentStatePagerAdapter handles fragment restoration.