Retrieve a Fragment from a ViewPager
I'm using a ViewPager
together with a FragmentStatePagerAdapter
to host three different fragments:
- [Fragment1]
- [Fragment2]
- [Fragment3]
When I want to get Fragment1
from the ViewPager
in the FragmentActivity
.
What is the problem, and how do I fix it?
Solution 1:
The main answer relies on a name being generated by the framework. If that ever changes, then it will no longer work.
What about this solution, overriding instantiateItem()
and destroyItem()
of your Fragment(State)PagerAdapter
:
public class MyPagerAdapter extends FragmentStatePagerAdapter {
SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return ...;
}
@Override
public Fragment getItem(int position) {
return MyFragment.newInstance(...);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}
This seems to work for me when dealing with Fragments that are available. Fragments that have not yet been instantiated, will return null when calling getRegisteredFragment
. But I've been using this mostly to get the current Fragment
out of the ViewPager
: adapater.getRegisteredFragment(viewPager.getCurrentItem())
and this won't return null
.
I'm not aware of any other drawbacks of this solution. If there are any, I'd like to know.
Solution 2:
For grabbing fragments out of a ViewPager there are a lot of answers on here and on other related SO threads / blogs. Everyone I have seen is broken, and they generally seem to fall into one of the two types listed below. There are some other valid solutions if you only want to grab the current fragment, like this other answer on this thread.
If using FragmentPagerAdapter
see below. If using FragmentStatePagerAdapter
its worth looking at this. Grabbing indexes that are not the current one in a FragmentStateAdapter is not as useful as by the nature of it these will be completely torn down went out of view / out of offScreenLimit bounds.
THE UNHAPPY PATHS
Wrong: Maintain your own internal list of fragments, added to when FragmentPagerAdapter.getItem()
is called
- Usually using a
SparseArray
orMap
- Not one of the many examples I have seen accounts for lifecycle events so this solution is fragile. As
getItem
is only called the first time a page is scrolled to (or obtained if yourViewPager.setOffscreenPageLimit(x)
> 0) in theViewPager
, if the hostingActivity
/Fragment
is killed or restarted then the internalSpaseArray
will be wiped out when the custom FragmentPagerActivity is recreated, but behind the scenes the ViewPagers internal fragments will be recreated, andgetItem
will NOT be called for any of the indexes, so the ability to get a fragment from index will be lost forever. You can account for this by saving out and restoring these fragment references viaFragmentManager.getFragment()
andputFragment
but this starts to get messy IMHO.
Wrong: Construct your own tag id matching what is used under the hood in FragmentPagerAdapter
and use this to retrieve the page Fragments from the FragmentManager
- This is better insomuch as it copes with the losing-fragment-references problem in the first internal-array solution, but as rightly pointed out in the answers above and elsewhere on the net - it feels hacky as its a private method internal to
ViewPager
that could change at any time or for any OS version.
The method thats recreated for this solution is
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
A HAPPY PATH: ViewPager.instantiateItem()
A similar approach to getItem()
above but non-lifecycle-breaking is to this is to hook into instantiateItem()
instead of getItem()
as the former will be called everytime that index is created / accessed. See this answer
A HAPPY PATH: Construct your own FragmentViewPager
Construct your own FragmentViewPager
class from the source of the latest support lib and change the method used internally to generate the fragment tags. You can replace it with the below. This has the advantage that you know the tag creation will never change and your not relying on a private api / method, which is always dangerous.
/**
* @param containerViewId the ViewPager this adapter is being supplied to
* @param id pass in getItemId(position) as this is whats used internally in this class
* @return the tag used for this pages fragment
*/
public static String makeFragmentName(int containerViewId, long id) {
return "android:switcher:" + containerViewId + ":" + id;
}
Then as the doc says, when you want to grab a fragment used for an index just call something like this method (which you can put in the custom FragmentPagerAdapter
or a subclass) being aware the result may be null if getItem has not yet been called for that page i.e. its not been created yet.
/**
* @return may return null if the fragment has not been instantiated yet for that position - this depends on if the fragment has been viewed
* yet OR is a sibling covered by {@link android.support.v4.view.ViewPager#setOffscreenPageLimit(int)}. Can use this to call methods on
* the current positions fragment.
*/
public @Nullable Fragment getFragmentForPosition(int position)
{
String tag = makeFragmentName(mViewPager.getId(), getItemId(position));
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
return fragment;
}
This is a simple solution and solves the issues in the other two solutions found everywhere on the web
Solution 3:
Add next methods to your FragmentPagerAdapter:
public Fragment getActiveFragment(ViewPager container, int position) {
String name = makeFragmentName(container.getId(), position);
return mFragmentManager.findFragmentByTag(name);
}
private static String makeFragmentName(int viewId, int index) {
return "android:switcher:" + viewId + ":" + index;
}
getActiveFragment(0) has to work.
Here is the solution implemented into ViewPager https://gist.github.com/jacek-marchwicki/d6320ba9a910c514424d. If something fail you will see good crash log.
Solution 4:
Another simple solution:
public class MyPagerAdapter extends FragmentPagerAdapter {
private Fragment mCurrentFragment;
public Fragment getCurrentFragment() {
return mCurrentFragment;
}
//...
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
}
Solution 5:
I know this has a few answers, but maybe this will help someone. I have used a relatively simple solution when I needed to get a Fragment
from my ViewPager
. In your Activity
or Fragment
holding the ViewPager
, you can use this code to cycle through every Fragment
it holds.
FragmentPagerAdapter fragmentPagerAdapter = (FragmentPagerAdapter) mViewPager.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
Fragment viewPagerFragment = fragmentPagerAdapter.getItem(i);
if(viewPagerFragment != null) {
// Do something with your Fragment
// Check viewPagerFragment.isResumed() if you intend on interacting with any views.
}
}
If you know the position of your
Fragment
in theViewPager
, you can just callgetItem(knownPosition)
.If you don't know the position of your
Fragment
in theViewPager
, you can have your childrenFragments
implement an interface with a method likegetUniqueId()
, and use that to differentiate them. Or you can cycle through allFragments
and check the class type, such asif(viewPagerFragment instanceof FragmentClassYouWant)
!!! EDIT !!!
I have discovered that getItem
only gets called by a FragmentPagerAdapter
when each Fragment
needs to be created the first time, after that, it appears the the Fragments
are recycled using the FragmentManager
. This way, many implementations of FragmentPagerAdapter
create new Fragment
s in getItem
. Using my above method, this means we will create new Fragment
s each time getItem
is called as we go through all the items in the FragmentPagerAdapter
. Due to this, I have found a better approach, using the FragmentManager
to get each Fragment
instead (using the accepted answer). This is a more complete solution, and has been working well for me.
FragmentPagerAdapter fragmentPagerAdapter = (FragmentPagerAdapter) mViewPager.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
String name = makeFragmentName(mViewPager.getId(), i);
Fragment viewPagerFragment = getChildFragmentManager().findFragmentByTag(name);
// OR Fragment viewPagerFragment = getFragmentManager().findFragmentByTag(name);
if(viewPagerFragment != null) {
// Do something with your Fragment
if (viewPagerFragment.isResumed()) {
// Interact with any views/data that must be alive
}
else {
// Flag something for update later, when this viewPagerFragment
// returns to onResume
}
}
}
And you will need this method.
private static String makeFragmentName(int viewId, int position) {
return "android:switcher:" + viewId + ":" + position;
}