I am trying to implement viewPager2 with variable number of fragments, and not sure on the correct / best approach and couldn't find any good examples to copy!
The fragments in my viewpager depend on the properties of the object being displayed - the first 2 are always present, but there are 4 more that may or may not be present - i.e. the FragmentStateAdapter does not know how many fragments there will be or which ones will be present until runtime.
My initial approach was to have a List that I passed to my FragmentStateAdapter which looked like this:
public class busDetailFragmentAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList;
public busDetailFragmentAdapter(#NonNull Fragment fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
// return fragments at every position
#NonNull
#Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
#Override
public int getItemCount() {
return fragmentList.size();
}
}
I then simply added fragments to my list with tabFragments.add(new busDetailInfoFrag()); and notified the fragmentadapter that an item had been inserted.
This worked fine until I navigated away from the parent fragment and then used the back button - the app crashed with a "Fragment already added" error.
I found something in the docs creating about new fragments and not reusing fragment. So, I modified my adapter to use a list of strings rather than a list of fragments, and am looking up the position against this list and using a switch statement to create the correct fragment.
public class busDetailFragmentAdapter extends FragmentStateAdapter {
private final List<String> fragmentList;
public busDetailFragmentAdapter(#NonNull Fragment fragmentActivity, List<String> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
// return fragments at every position
#NonNull
#Override
public Fragment createFragment(int position) {
switch (fragmentList.get(position)){
case "info":
return new busDetailInfoFrag();
case "location":
return new busDetailLocationFrag();
//More cases as needed
default:
return new missingFrag();
}
}
#Override
public int getItemCount() {
return fragmentList.size();
}
}
This is working as I need it to, but just wondering if this is the best/correct approach or if there's a better way!
I'm having some problems when dealing with Fragment using ViewPager.
What I'm having:
An activity (say, MainActivity) that contains a ViewPager to display some Fragment(s). Some of them contains a callback interface, which will be called to do somethings in the MainActivity.
The MainActivity has a FragmentPagerAdapter class, which is used as the adapter of the ViewPager. And a List<Fragment> in FragmentPagerAdapter to store some Fragment that will be displayed on the ViewPager.
What I'm expecting:
First launch, the Fragment called the callback interface's methods when I hit a button in it and MainActivity did somethings inside that. It worked great.
After a screen rotation, I expected it to work the same as the first launch BUT
NullPointerException: attempt to invoke a method on a null reference object (particularly, the Fragment's interface) hit me in my face.
What I know:
- The getItem(int position) won't be called again once the Fragment is created. Instead the instantiateItem(ViewGroup container, int position) will be called.
- FragmentManager will store some Fragment in mActive.mValues
- ViewPager and fragments — what's the right way to store fragment's state? (I did reference to this and some other same topics on StackOverflow too.)
What I have tried and saw:
- Override instantiateItem(ViewGroup container, int position)
- Debugged for 1 day. I saw that when I pass getSupportFragmentManager() in MainActivity's onCreate() method to FragmentPagerAdapter's super constructor, in the first launch, it has an "address in memory", assume it was '1a1a1a1'. The mActive.mValues of FragmentManager saved some Fragment' "address in memory" which are identical to the List<Fragment> containing them (assume it was 'qwertyu'). Which meaned it was right.
But when I rotated the screen, passing the getSupportFragmentManager() again, the "address in memory" was completely different, assume '9f9f9f9'. And FragmentManager's mActive.mValues contained a different set of Fragment' "address in memory" too (assume 'abcdeff'), although the number of Fragment in it was equal to the number of Fragment that was saved on the first launch (before rotation).
I have added a Fragment to the List<Fragment> with a new "address in memory" (assume 'abababa'), has the callback interface. But when I hit the button in it, it was the Fragment that was in the FragmentManager's mActive.mValues after the rotation (with "address in memory" is 'abcdeff' as I assumed above), and that one didn't have the callback interface (due to not being set in MainActivity first). And caused the NullPointerException as mentioned above.
My questions now is:
- First of all, how to get rid of this problem!? It would be better to keep using FragmentPagerAdapter instead of another class. But I will consider using other class too.
- Second, can you explain why FragmentManager saved the Fragment instance before rotation. But after rotation, it creates a completely different Fragment instance but still uses it instead of the Fragment that was saved in the List<Fragment>?
Here is the code (I think I didn't use the instantiateItem(ViewGroup container, int position) method in the right way so it still caused the problem).
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Attach the SectionsPagerAdapter to the ViewPager
SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(pagerAdapter);
}
//
//
//Adapter class
private class SectionsPagerAdapter extends FragmentPagerAdapter {
private static final int PAGE_HOME = 0;
private int tabCount = 1;
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
//private FragmentManager fragmentManager;
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
//fragmentManager = fm;
//Default HomeFragment
HomeFragment homeFragment = new HomeFragment();
//Callback interface
homeFragment.setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
//This method will be called when a button in HomeFragment is clicked
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
fragmentList.add(homeFragment);
fragmentTitleList.add("Home");
}
#Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
#Override
public int getCount() {
return tabCount;
}
#Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
return fragmentList.get(position);
}
private void addNewCategory(String categoryName) {
CategoryFragment fragment = new CategoryFragment();
tabCount += 1;
fragmentList.add(fragment);
fragmentTitleList.add(categoryName);
notifyDataSetChanged();
}
}
}
Please help. I'm being insane for 2 days now...!
I believe android restarts the activity during orientation change thus making multiple instances of FragmentPagerAdapter and multiple set of instances of List.
I don't completely understand your question but I suspect instantiateItem doesn't do anything anyway. Doesn't the Fragment getItem(int pos) work without overriding instantiateItem()?
Oh well, right after i felt in sleep, I found the solution. It's true that I didn't use the instantiateItem() method in the right way. After debugging again, I found that the instantiateItem() method get call whenever I swipe (or choose if using TabLayout as well) to another Fragment, and even get call before getItem(int pos), no matter what it's the first launch or after rotation. Which is why I think we should set things up for the Fragment in the instantiateItem() method.
So here is how I use the instantiateItem() method now:
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
Fragment fragment = fragmentList.get(position);
if (position == PAGE_HOME) {
((HomeFragment) fragment).setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
}
return fragment;
}
If anyone have a better solution, please just tell me if you don't mind. I will consider about it.
I'm facing some issues with ViewPager and Fragment's instances.
I have a ViewPager (let's call Father) with 4 fragments and into the last fragment I have another ViewPager (and call it Child) with dynamic fragments amount. What I mean is that I create the Child based on a list of objects in memory. So if the list contains 3 objets, the Child will have 3 fragments inside. If in a determinate moment something happens and I get a list with 1 object, then I must update the Child with just 1 fragment. An important point in here is that each Child'sFragment has its own object from the returned list and is created based on this object.
The code I do to set the list of fragments into the Child ViewPager is the following:
#Override
public void setViewPagerChildFragments(List<Fragment> fragments) {
if (fragments != null) {
DefaultStateViewPagerAdapter adapter = (DefaultStateViewPagerAdapter) mViewpagerChild.getAdapter();
if (adapter == null) {
/* In this case I use getChildFragmentManager() because
it's inside the last fragment of the ViewPager Father*/
adapter = new DefaultStateViewPagerAdapter(getChildFragmentManager(), fragments);
mViewpagerChild.setAdapter(adapter);
} else {
adapter.setFragments(fragments); // Into this method I do notifyDataSetChanged() already
}
}
}
Note that I try to use the same adapter's instance to set the fragments and then notify the changes (notifyDataSetChanged() is inside the method). If I don't get the adapter's instance, I create a new one and set it to the ViewPager Child.
The problem happens, for example, when I set the ViewPager Child with 2 fragments and after a while I need to set it with 1 fragment. The ViewPager shows just 1 fragment inside it, but the second one is still attached and isn't destroyed. I know it because I did a test calling getChildFragmentManager().getFragments(), and I could see the fragment which was supposed to be destroyed is still there.
You may say it isn't actually a problem, since the Garbage Collector can remove the unused Fragment. However, if in some moment for example, I try to set 2 new fragments again into the ViewPager Child, it uses that unused Fragment instance instead of creating a new one and unfortunately it also uses its same object, and not the right new one.
This is my DefaultStateViewPagerAdaptercode:
public class DefaultStateViewPagerAdapter extends FragmentStatePagerAdapter {
private ArrayList<Fragment> mFragments;
private ArrayList<String> mFragmentTitles;
public DefaultStateViewPagerAdapter(FragmentManager fm) {
super(fm);
this.mFragments = new ArrayList<>();
this.mFragmentTitles = new ArrayList<>();
}
public DefaultStateViewPagerAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.mFragments = (ArrayList<Fragment>) fragments;
}
#Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
#Override
public int getItemPosition(Object object) {
int index = mFragments.indexOf(object);
if (index < 0) {
index = POSITION_NONE;
}
return index;
}
#Override
public int getCount() {
return mFragments.size();
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitles.get(position);
}
public void clearFragments() {
mFragments.clear();
mFragmentTitles.clear();
}
public void setFragments(List<Fragment> fragments) {
mFragments = (ArrayList<Fragment>) fragments;
notifyDataSetChanged();
}
public void addFragment(Fragment fragment, String title) {
mFragments.add(fragment);
mFragmentTitles.add(title);
}
}
I already tried to override saveState in order to avoid the ViewPager uses the old fragment´s instance, like:
#Override
public Parcelable saveState() {
return null;
}
It worked, and the ViewPager no longer uses the old reference, but the unused Fragment is still attached, and it causes memory leak.
I don't know why the ViewPager doesn't destroy its fragments even after I set a new adapter. Has anyone ever had this issue?
I found the solution.
The solution is very simple. I just had to set manually null to the Child's adapter. With this, the ViewPager is forced to destroy every fragment.
So into the onDestroyView of Fragment's father I added:
#Override
public void onDestroyView() {
super.onDestroyView();
mViewpagerChild.removeOnPageChangeListener(mOnPaymentMethodsPageChangeListener);
mViewpagerChild.setAdapter(null); // <-- This is what I added
}
I'm very new to Android development and I have been trying make an application that allows users to flip either to the left or right to change fragments. I'm using a FragmentStatePagerAdapter for this.
I first, in a for loop, created each fragment separately in the main activity's OnCreate() method and added them to the FragmentManager fManager.
for(Day day: days){
ScreenSlidePageFragment fragment = new ScreenSlidePageFragment();
this.fManager.beginTransaction().add(R.id.pager, fragment).commit();
}
fManager.executePendingTransactions();
I then set up UI elements and set the ViewPager's adapter like so
this.setUpUI();
mPager.setAdapter(mPagerAdapter);
The problem is is that I keep encountering this exception java.lang.IllegalStateException: Fragment Already Added...
I am very confused as to what is going on because the only place I add fragments to the FragmentManager is in the for-loop above. Do FragmentStatePagerAdapter's do something in the background that might cause this issue or am I not implementing my ScreenSlidePagerAdapter correctly?
Here is my code for the ScreenSlidePagerAdapter class:
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public FragmentManager fm;
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm);
this.fm = fm;
}
#Override
public Fragment getItem(int position) {
Fragment f = fm.getFragments().get(position);
return f;
}
#Override
public int getCount() {
return fm.getFragments().size();
}
}
Input would be greatly appreciated.
You show create the fragments inside getItem(pos) method of Adapter. The adapter internally use the FragmentManager that you provide in constructor.
Some what like this:
#Override
public Fragment getItem(int position) {
Fragment f = new ScreenSlidePageFragment();
return f;
}
I think the FragmentStatePagerAdapter already adds the fragments to your Activity (in the getItem() method). Try to remove that for loop.
You are getting the Fragment that is already in the FragmentManager and when you return it, the PagerAdapter tries to add it, but it is already there.
You don't have to add the fragments to the FragmentManager yourself. That's the FragmentStatePagerAdapter's job, that's why it receives a FragmentManager reference. You only have to create and return the fragments in getItem() and the PagerAdapter will add the fragments to the FragmentManager.
I usually do what you are trying like this:
public class FragmentSliderAdapter extends FragmentStatePagerAdapter {
private List<Fragment> list = new ArrayList<Fragment>();
public FragmentSliderAdapter(FragmentManager fm) {
super(fm);
list.add(new LoginFragment());
list.add(new SignupFragment());
}
#Override
public Fragment getItem(int arg0) {
return list.get(arg0);
}
#Override
public int getCount() {
return list.size();
}
}
Using list is cleaner and a good approach, since you can control what fragments are added to your adapter.
On the activity that uses the adapter, I just do this:
viewPager = (ViewPager) findViewById(R.id.pager);
adapter = new FragmentSliderAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
And the activity xml I do:
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
See if it helps !
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.
Summary of the problem
Note: In this answer I'm going to reference FragmentPagerAdapter and its source code. But the general solution should also apply to FragmentStatePagerAdapter.
If you're reading this you probably already know that FragmentPagerAdapter/FragmentStatePagerAdapter is meant to create Fragments for your ViewPager, but upon Activity recreation (whether from a device rotation or the system killing your App to regain memory) these Fragments won't be created again, but instead their instances retrieved from the FragmentManager. Now say your Activity needs to get a reference to these Fragments to do work on them. You don't have an id or tag for these created Fragments because FragmentPagerAdapter set them internally. So the problem is how to get a reference to them without that information...
Problem with current solutions: relying on internal code
A lot of the solutions I've seen on this and similar questions rely on getting a reference to the existing Fragment by calling FragmentManager.findFragmentByTag() and mimicking the internally created tag: "android:switcher:" + viewId + ":" + id. The problem with this is that you're relying on internal source code, which as we all know is not guaranteed to remain the same forever. The Android engineers at Google could easily decide to change the tag structure which would break your code leaving you unable to find a reference to the existing Fragments.
Alternate solution without relying on internal tag
Here's a simple example of how to get a reference to the Fragments returned by FragmentPagerAdapter that doesn't rely on the internal tags set on the Fragments. The key is to override instantiateItem() and save references in there instead of in getItem().
public class SomeActivity extends Activity {
private FragmentA m1stFragment;
private FragmentB m2ndFragment;
// other code in your Activity...
private class CustomPagerAdapter extends FragmentPagerAdapter {
// other code in your custom FragmentPagerAdapter...
public CustomPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// Do NOT try to save references to the Fragments in getItem(),
// because getItem() is not always called. If the Fragment
// was already created then it will be retrieved from the FragmentManger
// and not here (i.e. getItem() won't be called again).
switch (position) {
case 0:
return new FragmentA();
case 1:
return new FragmentB();
default:
// This should never happen. Always account for each position above
return null;
}
}
// Here we can finally safely save a reference to the created
// Fragment, no matter where it came from (either getItem() or
// FragmentManger). Simply save the returned Fragment from
// super.instantiateItem() into an appropriate reference depending
// on the ViewPager position.
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// save the appropriate reference depending on position
switch (position) {
case 0:
m1stFragment = (FragmentA) createdFragment;
break;
case 1:
m2ndFragment = (FragmentB) createdFragment;
break;
}
return createdFragment;
}
}
public void someMethod() {
// do work on the referenced Fragments, but first check if they
// even exist yet, otherwise you'll get an NPE.
if (m1stFragment != null) {
// m1stFragment.doWork();
}
if (m2ndFragment != null) {
// m2ndFragment.doSomeWorkToo();
}
}
}
or if you prefer to work with tags instead of class member variables/references to the Fragments you can also grab the tags set by FragmentPagerAdapter in the same manner:
NOTE: this doesn't apply to FragmentStatePagerAdapter since it doesn't set tags when creating its Fragments.
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// get the tags set by FragmentPagerAdapter
switch (position) {
case 0:
String firstTag = createdFragment.getTag();
break;
case 1:
String secondTag = createdFragment.getTag();
break;
}
// ... save the tags somewhere so you can reference them later
return createdFragment;
}
Note that this method does NOT rely on mimicking the internal tag set by FragmentPagerAdapter and instead uses proper APIs for retrieving them. This way even if the tag changes in future versions of the SupportLibrary you'll still be safe.
Don't forget that depending on the design of your Activity, the Fragments you're trying to work on may or may not exist yet, so you have to account for that by doing null checks before using your references.
Also, if instead you're working with FragmentStatePagerAdapter, then you don't want to keep hard references to your Fragments because you might have many of them and hard references would unnecessarily keep them in memory. Instead save the Fragment references in WeakReference variables instead of standard ones. Like this:
WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
// reference hasn't been cleared yet; do work...
}
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 the FragmentPagerAdapter is 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 like createItem(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 FragmentPagerAdapter and 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 the FragmentManager by calling findFragmentByTag(). 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
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.
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.
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;
}
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
}
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;
}
}
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;
}
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)
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);
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.
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.
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);
}
}
Just go on try this code,
public class MYFragmentPAdp extends FragmentPagerAdapter {
public MYFragmentPAdp(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return 2;
}
#Override
public Fragment getItem(int position) {
if (position == 0)
Fragment fragment = new Fragment1();
else (position == 1)
Fragment fragment = new Fragment2();
return fragment;
}
}