'How to get existing fragments when using FragmentPagerAdapter
I have problem making my fragments communicating with each other through the Activity, which is using the FragmentPagerAdapter, as a helper class that implements the management of tabs and all details of connecting a ViewPager with associated TabHost. I have implemented FragmentPagerAdapter just as same as it is provided by the Android sample project Support4Demos.
The main question is how can I get particular fragment from FragmentManager when I don't have neither Id or Tag? FragmentPagerAdapter is creating the fragments and auto generating the Id and Tags.
Solution 1:[1]
I have found answer on my question based on following post: reusing fragments in a fragmentpageradapter
Few things I have learned:
getItem(int position)in theFragmentPagerAdapteris rather misleading name of what this method actually does. It creates new fragments, not returning existing ones. In so meaning, the method should be renamed to something likecreateItem(int position)in the Android SDK. So this method does not help us getting fragments.- Based on explanation in the post support FragmentPagerAdapterholds reference to old fragments you should leave the creation of the fragments to the
FragmentPagerAdapterand in so meaning you have no reference to the Fragments or their tags. If you have fragment tag though, you can easily retrieve reference to it from theFragmentManagerby callingfindFragmentByTag(). We need a way to find out tag of a fragment at given page position.
Solution
Add following helper method in your class to retrieve fragment tag and send it to the findFragmentByTag() method.
private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
return "android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
NOTE! This is identical method that FragmentPagerAdapter use when creating new fragments. See this link http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/android/support/v2/app/FragmentPagerAdapter.java#104
Solution 2:[2]
you don't need to override instantiateItem nor rely on compatibility with internal makeFragmentName method by manually creating fragment tags .instantiateItem is a public method so you can call it in onCreate method of your activity surrounded with calls to startUpdate and finishUpdate methods as described in PagerAdapter javadoc:
A call to the PagerAdapter method startUpdate(ViewGroup) indicates that the contents of the ViewPager are about to change. One or more calls to instantiateItem(ViewGroup, int) and/or destroyItem(ViewGroup, int, Object) will follow, and the end of an update will be signaled by a call to finishUpdate(ViewGroup).
You can then by the way of the above, store references to instances of your fragments on local vars if you need. See example:
public class MyActivity extends AppCompatActivity {
Fragment0 tab0; Fragment1 tab1;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
((TabLayout) findViewById(R.id.tabs)).setupWithViewPager(viewPager);
adapter.startUpdate(viewPager);
tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
adapter.finishUpdate(viewPager);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager manager) {super(manager);}
@Override public int getCount() {return 2;}
@Override public Fragment getItem(int position) {
if (position == 0) return new Fragment0();
if (position == 1) return new Fragment1();
return null; // or throw some exception
}
@Override public CharSequence getPageTitle(int position) {
if (position == 0) return getString(R.string.tab0);
if (position == 1) return getString(R.string.tab1);
return null; // or throw some exception
}
}
}
instantiateItem will first try to get references to existing fragment instances from FragmentManager. Only if they don't exist yet, it will create new ones using getItem method from your adapter and "store" them in the FragmentManager for any future use.
UPDATE 05/2022: according to this comment the below part may lead to suboptimal performance in the current implementation.
Following the above javadoc, you still should call instantiateItem for all your tabs surrounded by startUpdate/finishUpdate in your onCreate method even if you don't need to obtain references to your fragments:
adapter.startUpdate(viewPager);
// ignoring return values of the below 2 calls, just side effects matter:
adapter.instantiateItem(viewPager, 0);
adapter.instantiateItem(viewPager, 1);
adapter.finishUpdate(viewPager);
If you don't do so, then you are risking that your fragment instances will never be committed to FragmentManager : when your activity becomes foreground instantiateItem will be called automatically to obtain your fragments, but startUpdate/finishUpdate may not (depending on implementation details) and what they basically do is begin/commit a FragmentTransaction.
This may result in references to the created fragment instances being lost very quickly (for example when you rotate your screen) and recreated much more often than necessary. Depending on how "heavy" your fragments are, it may have a non-negligible performance consequences.
Moreover, in such case instances of fragments stored on local vars may become stale: if android platform tries to obtain them from FragmentManager for whatever reason, it will fail and thus will create and use new ones, while your vars will still be referencing the old ones.
Solution 3:[3]
The way I did it is define an Hashtable of WeakReferences as follows:
protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;
Then I wrote the getItem() method like this:
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch(position) {
case 0:
fragment = new MyFirstFragmentClass();
break;
default:
fragment = new MyOtherFragmentClass();
break;
}
fragmentReferences.put(position, new WeakReference<Fragment>(fragment));
return fragment;
}
Then you can write a method:
public Fragment getFragment(int fragmentId) {
WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
return ref == null ? null : ref.get();
}
This seems to work well and I find it a little less hacky than the
"android:switcher:" + viewId + ":" + position
trick, as it does not rely on how the FragmentPagerAdapter is implemented. Of course if the fragment has been released by the FragmentPagerAdapter or if it has not been yet created, getFragment will return null.
If anybody finds something wrong with this approach, comments are more than welcome.
Solution 4:[4]
I created this method which is working for me to get a reference to the current fragment.
public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
try {
Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
f.setAccessible(true);
FragmentManager fm = (FragmentManager) f.get(adapter);
m.setAccessible(true);
String tag = null;
tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
return fm.findFragmentByTag(tag);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
Solution 5:[5]
FragmentStateAdapter for ViewPager2 UPDATE
FragmentStateAdapter has createFragment() instead of getItem(). And there is no such method as instantiateView().
So when the hosting Activity/Fragment is recreated after the configuration change, createFragment() doesn't get called. This means that fragments in the adapter are not created again, but instead their instances are retrieved from FragmentManager.
So if you need to do some work on the fragments, you can simply get them from FragmentManager.
Adapter creation:
CustomFragmentAdapter adapter = new CustomFragmentAdapter(getChildFragmentManager(), getLifecycle());
Retrieving fragments from FragmentManager after Activity/Fragment reload:
FragmentManager manager = getChildFragmentManager();
ArrayList<Fragment> fragments = new ArrayList<>(manager.getFragments());
for (Fragment fr : fragments) {
// do something
}
Solution 6:[6]
the solution suggested by @personne3000 is nice, but it has one problem: when activity goes to the background and gets killed by the system (in order to get some free memory) and then restored, the fragmentReferences will be empty, because getItem wouldn't be called.
The class below handles such situation:
public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {
public static final String FRAGMENT_SAVE_PREFIX = "holder";
private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters.
public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
super(fm);
fragmentManager = fm;
}
private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();
protected void holdFragment(F fragment) {
holdFragment(holder.size(), fragment);
}
protected void holdFragment(int position, F fragment) {
if (fragment != null)
holder.put(position, new WeakReference<F>(fragment));
}
public F getHoldedItem(int position) {
WeakReference<F> ref = holder.get(position);
return ref == null ? null : ref.get();
}
public int getHolderCount() {
return holder.size();
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation
super.restoreState(state, loader);
Bundle bundle = (Bundle) state;
for (String key : bundle.keySet()) {
if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
Fragment f = fragmentManager.getFragment(bundle, key);
holdFragment(index, (F) f);
}
}
}
@Override
public Parcelable saveState() {
Bundle state = (Bundle) super.saveState();
if (state == null)
state = new Bundle();
for (int i = 0; i < holder.size(); i++) {
int id = holder.keyAt(i);
final F f = getHoldedItem(i);
String key = FRAGMENT_SAVE_PREFIX + id;
fragmentManager.putFragment(state, key, f);
}
return state;
}
}
Solution 7:[7]
The main road block with getting a handle to the fragments is you can not rely on getItem(). After an orientation change, references to the fragments will be null and getItem() is not called again.
Here's an approach that does not rely upon the implementation of FragmentPagerAdapter to get the tag. Override instantiateItem() which will return the fragment created from getItem() or found from the fragment manager.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object value = super.instantiateItem(container, position);
if (position == 0) {
someFragment = (SomeFragment) value;
} else if (position == 1) {
anotherFragment = (AnotherFragment) value;
}
return value;
}
Solution 8:[8]
See this post on returning fragments from the FragmentPagerAdapter. Does rely on you knowing the index of your fragment - but this would be set in getItem() (at instantiation only)
Solution 9:[9]
I managed to solve this issue by using ids instead of tags. (I am using I defined FragmentStatePagerAdapter which uses my custom Fragments in which I overrode the onAttach method, where you save the id somewhere:
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
And then you just access the fragment easily inside the activity:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
Solution 10:[10]
I don't know if this is the best approach but nothing else worked for me. All other options including getActiveFragment returned null or caused the app to crash.
I noticed that on screen rotation the fragment was being attached so I used it to send the fragment back to the activity.
In the fragment:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnListInteractionListener) activity;
mListener.setListFrag(this);
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
Then in the activity:
@Override
public void setListFrag(MyListFragment lf) {
if (mListFragment == null) {
mListFragment = lf;
}
}
And finally in activity onCreate():
if (savedInstanceState != null) {
if (mListFragment != null)
mListFragment.setListItems(items);
}
This approach attaches the actual visible fragment to the activity without creating a new one.
Solution 11:[11]
Not sure if my method was the correct or best way to do this since I am a relative beginner with Java/Android, but it did work (I'm sure it violates object oriented principles but no other solution worked for my use case).
I had a hosting Activity that was using a ViewPager with a FragmentStatePagerAdapter. In order to get references to the Fragments that were created by FragmentStatePagerAdapter I created a callback interface within the fragment class:
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
In the hosting activity I implemented the interface and created a LinkedHasSet to keep track of the fragments:
public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {
private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();
@Override
public void addFragment (Fragment fragment) {
mFragments.add(fragment);
}
@Override
public void removeFragment (Fragment fragment) {
mFragments.remove(fragment);
}
}
Within the ViewPagerFragment class I added the fragments to the list within onAttach and removed them within onDetach:
public class ViewPagerFragment extends Fragment {
private Callbacks mCallbacks;
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
// Add this fragment to the HashSet in the hosting activity
mCallbacks.addFragment(this);
}
@Override
public void onDetach() {
super.onDetach();
// Remove this fragment from the HashSet in the hosting activity
mCallbacks.removeFragment(this);
mCallbacks = null;
}
}
Within the hosting activity you'll now be able to use mFragments to iterate through the fragments that currently exist in the FragmentStatePagerAdapter.
Solution 12:[12]
This class do the trick without relying on internal tags. Warning: Fragments should be accessed using the getFragment method and not the getItem one.
public class ViewPagerAdapter extends FragmentPagerAdapter {
private final Map<Integer, Reference<Fragment>> fragments = new HashMap<>();
private final List<Callable0<Fragment>> initializers = new ArrayList<>();
private final List<String> titles = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
void addFragment(Callable0<Fragment> initializer, String title) {
initializers.add(initializer);
titles.add(title);
}
public Optional<Fragment> getFragment(int position) {
return Optional.ofNullable(fragments.get(position).get());
}
@Override
public Fragment getItem(int position) {
Fragment fragment = initializers.get(position).execute();
return fragment;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
fragments.put(position, new WeakReference<>(fragment));
return fragment;
}
@Override
public int getCount() {
return initializers.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
