Using SDK 19, min 13, and support.v4.app Fragments.
I have searched SO and found similiar threads which should help, but everything I have tried has not addressed my issue yet. Furthermore, many people seem to have the issue of the ViewPager restarting when they don't want it to, whereas my problem seems to be the opposite.
These posts do not seem to have helped me yet:
PagerAdapter start position
ViewPager PagerAdapter not updating the View
How to force ViewPager to re-instantiate its items
Here is my setup:
A (support.v4) Fragment in memory, which contains a ViewPager, inflated from a layout file, thus not added programmatically. The Fragment itself is not recreated, but in memory for the life of the app; it is being attached/detached to the root FragmentActivity's FragmentManager.
When the user visits this Fragment sometime during the app's lifetime, it is attached and the onCreateView is called, where I findViewById the ViewPager and assign a new FragmentStatePagerAdapter to it.
Here is my problem:
The first time the user visits this pager, everything works fine. The next time they visit it, I expect it to "start over" from page 0, meaning that I expect to not be able to scroll left immediately, but must start scrolling right. I also expect the getItem() of the FragmentStatePagerAdapter to start back at position 0. I basically want it to function the same as the first time the user visited it. However, this is not the case.
I have tried several things, but it always seems to start at the previous index of page I left it at. So if the first time I scrolled 5 pages over, and then left the Fragment and returned later, the ViewPager starts at that same page index, meaning I can scroll 5 pages left. I don't want this.
It may have something to do with the ViewPager or FragmentStatePagerAdapter storing pages internally, which are not being released. But I thought the commented out code I tried would have done that. There might be another way that I have not tried. Perhaps it is related to not recreating my Fragment, but doing so at this point in my architecture will not be great. I was hoping I could restart the ViewPager without doing this.
Here is my code:
My Fragment's onCreateView code. Everything commented out I have tried without success.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_playscreen, container, false);
//while (getChildFragmentManager().popBackStackImmediate()) {}
mAdapter = new AdapterPlayPages(this, getChildFragmentManager());
mPager = (ViewPlayPager) view.findViewById(R.id.pagerPlayScreen);
//mPager.setAdapter(null);
mPager.storeAdapter(mAdapter);
//mPager.setCurrentItem(0);
//mPager.removeAllViews();
return view;
}
My ViewPager, which has been overridden like so to get around a completely unrelated bug, as suggested here: https://stackoverflow.com/a/19900206/1002098. This does not effect the problem; I removed these changes so that I could call setAdapter(null) as suggested, but it did not address my question.
public class ViewPlayPager extends ViewPager
{
PagerAdapter mPagerAdapter;
public ViewPlayPager(Context context)
{
super(context);
}
public ViewPlayPager(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (mPagerAdapter != null)
{
//super.setAdapter(null); // did not help
super.setAdapter(mPagerAdapter);
}
}
#Override
public void setAdapter(PagerAdapter adapter)
{
// do nothing
}
public void storeAdapter(PagerAdapter adapter)
{
mPagerAdapter = adapter;
}
}
My FragmentStatePagerAdapter. I've removed some unrelated logic and members.
public class AdapterPlayPages extends FragmentStatePagerAdapter
{
private FragmentPlayScreen mParent;
public AdapterPlayPages(FragmentPlayScreen parent, FragmentManager fragmentManager)
{
super(fragmentManager);
mParent = parent;
}
#Override
public int getCount()
{
// some logic from parent to determine true count
// the count shouldn't effect the position to start at
return Integer.MAX_VALUE;
}
#Override
public Fragment getItem(int position)
{
// logic to determine type of page to display,
// which is not changing based on position at the moment
return new FragmentPlayPage();
}
// this did not seem to help me
//#Override
//public int getItemPosition(Object object)
//{
// //return super.getItemPosition(object);
// return POSITION_NONE;
//}
}
I had to change my architecture to recreate the Fragments - add/remove them from FragmentManager, as opposed to attach/detach them while kept in memory.
It is not clear to me why the Fragment would have to be recreated for the ViewPager to reset. Android FragmentManager and FragmentTransaction supports keeping Fragments in memory and simply attaching/detaching them, or showing/hiding them, so it should be possible to simply reset the ViewPager from the in-memory Fragment's onCreateView event, but again, none of the commented out calls above seemed to work.
If anyone else has a solution, I'd still consider it. It will help me understand this issue.
Related
Trying to programmatically add a fragment page to my ViewPager, I get:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged!
Expected adapter item count: 3, found: 2
Pager id: com.my.app:id/view_pager
Pager class: class android.support.v4.view.ViewPager
Problematic adapter: class com.my.app.ui.BaseFragmentPagerAdapter
at android.support.v4.view.ViewPager.populate(ViewPager.java:1000)
at android.support.v4.view.ViewPager.populate(ViewPager.java:952)
at ...
I'm simply calling these few lines on my FragmentPagerAdapter implementation:
adapter.addFragment(new Fragment(), "FIRST");
adapter.addFragment(new Fragment(), "SECOND");
pager.setAdapter(adapter);
//later... (on click of a button)
adapter.addFragment(new Fragment(), "THIRD");
adapter.notifyDataSetChanged();
It actually adds the third page, but when I try to swipe there, it fails with the above mentioned exception. Until today I thought I had a pretty complete understanding of how adapters work. Now I can't figure out what's wrong.
From debugging, it seems that all the time adapter.getCount() correctly returns 3 (after adding the third page), but when I'm there to the third page it eventually returns 2 and breaks, as if someone called destroyItem() on it, but that's not me.
Here's my simple class:
public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
private SparseArray<Fragment> mFragments;
private ArrayList<String> mFragmentTitles;
public BaseFragmentPagerAdapter(FragmentManager manager) {
super(manager);
this.mFragments = new SparseArray<>();
this.mFragmentTitles = new ArrayList<>();
}
public void addFragment(Fragment f, String title) {
this.mFragments.append(mFragments.size() , f);
this.mFragmentTitles.add(title);
}
#Override
public Fragment getItem(int position) {
return this.mFragments == null ? null : this.mFragments.get(position) ;
}
#Override
public int getItemPosition(Object object) {
return this.mFragments.indexOfValue((Fragment) object);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
this.mFragments.remove(position);
this.mFragmentTitles.remove(position);
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitles.get(position);
}
#Override
public int getCount() {
return mFragmentTitles.size();
}
}
Note that nothing changes if I use a FragmentStatePagerAdapter rather than FragmentPagerAdapter.
I will answer this myself since I found the answer while writing the question (as often). I'm not sure this is the best solution, but it worked.
Basically, when going to page 3, since it's not directly swipable-to, the adapter will call destroyItem() on page 1. Shouldn't FragmentPagerAdapter hold all items in memory without destroying them?
Well, I do hold fragments in memory through the mFragments fields. The call to destroyItem() destroys the associated view of the fragment, but should not destroy the fragment itself (I might be slightly wrong here, but you get the point).
So it's up to you (me) to keep the fragments in memory, and not invalidating them on destroyItem(). Specifically, I had to remove these two lines:
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
//removed: this.mFragments.remove(position);
//removed: this.mFragmentTitles.remove(position);
}
This way getCount() keeps returning correctly 3, and when you are back to page 1, the adapter can get its fragment through getItem().
Edit
After dealing with it for a day, I can say that, at a first glance, having a FragmentPagerAdapter that does not hold fragments in memory makes no sense to me.
It is documented that it should be used just for a few static fragments. ViewPager, by default, holds the current item, the one before and the one after, but you can tune this setting through viewPager.setOffscreenPageLimit().
If you destroy them during destroyItem(), things get bad. It will be easy to reinstantiate them through some logic in getItem(), but it is quite hard to save their instance state. For instance, onSaveInstanceState(Bundle outstate) is not called after destroyItem().
If your fragments are many and you want to accept the possibility that one of more get destroyed, you just switch to FragmentStatePagerAdapter, but that's another story: it automatically calls onSaveInstanceState and lets you retain what you need to retain.
Using FragmentPagerAdapter, thus renouncing on the state-saving features of FragmentStatePagerAdapter, makes no sense if you don't retain. I mean, either you retain the instances, or you save their state (as suggested in the comments). For the latter, though, I would go for FragmentStatePagerAdapter that makes it easy.
(Note: I'm not talking about retaining instances when the activity gets destroyed, but rather when a page of the ViewPager goes through destroyItem and the associated fragment goes through onDestroyView()).
I've got this strange issue, ViewPager's setCurrentItem(position, false) works perfectly fine, then im switching to another activity, and after I'm back to the first activity, the ViewPager always ends up on the first item. Even though I've added setCurrentItem to onResume method it still ignores it. It's not even throwing any exception when I'm trying to set item to out of bounds index.
Though later on when I call this method, when the button "next" is tapped, it works like expected.
Checked my code 10 times for any possible calls to setCurrentItem(0) or something but it's just not there at all.
i can't really answer WHY exactly this happens, but if you delay the setCurrentItem call for a few milliseconds it should work. My guess is that because during onResume there hasn't been a rendering pass yet, and the ViewPager needs one or something like that.
private ViewPager viewPager;
#Override
public void onResume() {
final int pos = 3;
viewPager.postDelayed(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(pos);
}
}, 100);
}
UPDATE: story time
so today i had the problem that the viewpager ignored my setCurrentItem action, and i searched stackoverflow for a solution. i found someone with the same problem and a fix; i implemented the fix and it didn't work. whoa! back to stackoverflow to downvote that faux-fix-provider, and ...
it was me. i implemented my own faulty non-fix, which i came up with the first time i stumbled over the problem (and which was later forgotten). i'll now have to downvote myself for providing bad information.
the reason my initial "fix" worked was not because of of a "rendering pass"; the problem was that the pager's content was controlled by a spinner. both the spinners and the pagers state were restored onResume, and because of this the spinners onItemSelected listener was called during the next event propagation cycle, which did repopulate the viewpager - this time using a different default value.
removing and resetting the listener during the initial state restoration fixed the issue.
the fix above kind-of worked the first time, because it set the pagers current position after the onItemSelected event fired. later, it ceased to work for some reason (probably the app became too slow - in my implementation i didn't use 100ms, but 10ms). i then removed the postDelayed in a cleanup cycle, because it didn't change the already faulty behaviour.
update 2: i can't downvote my own post. i assume, honorable seppuku is the only option left.
I had a similar issue in the OnCreate of my Activity.
The adapter was set up with the correct count and I
applied setCurrentItem after setting the adapter to the
ViewPager however is would return index out of bounds. I think the ViewPager had not loaded all my Fragments at the point i set the current item. By posting a runnable on the ViewPager i was able to work around this. Here is an example with a little bit of context.
// Locate the viewpager in activity_main.xml
final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
// Set the ViewPagerAdapter into ViewPager
viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager()));
viewPager.setOffscreenPageLimit(2);
viewPager.post(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(ViewPagerAdapter.CENTER_PAGE);
}
});
I found a very simple workaround for this:
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
And, if that doesn't work, you can put it in a handler, but there's no need for a timed delay:
new Handler().post(new Runnable() {
#Override
public void run() {
mViewPager.setCurrentItem(desiredPos);
}
});
ViewTreeObserver can be used to avoid a static delay.
Kotlin:
Feel free to use Kotlin extension as a concise option.
view_pager.doOnPreDraw {
view_pager.currentItem = 1
}
Please, make sure you have a gradle dependency: implementation 'androidx.core:core-ktx:1.3.2' or above
Java
OneShotPreDrawListener.add(view_pager, () -> view_pager.currentItem = 1);
A modern approach in a Fragment or Activity is to call ViewPager.setcurrentItem(Int) function in a coroutine in the context of Dispatchers.Main :
lifecycleScope.launch(Dispatchers.Main) {
val index = 1
viewPager.setCurrentItem(index)
}
I had similar bug in the code, the problem was that I was setting the position before changing the data.
The solution was simply to set the position afterwards and notify the data changed
notifyDataSetChanged()
setCurrentItem()
I have the same problem and I edit
#Override
public int getCount() { return NUM_PAGES; }
I set NUM_PAGES be mistake to 1 only.
some guy wrote on forums here. https://code.i-harness.com/en/q/126bff9 worked for me
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
Solution (in Kotlin with ViewModel etc.) for those trying to set the current item in the onCreate of Activity without the hacky Runnable "solutions":
class MyActivity : AppCompatActivity() {
lateinit var mAdapter: MyAdapter
lateinit var mPager: ViewPager
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_pager)
// ...
mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
mAdapter = MyAdapter(supportFragmentManager)
mPager = findViewById(R.id.pager)
mainViewModel.someData.observe(this, Observer { items ->
items?.let {
// first give the data to the adapter
// this is where the notifyDataSetChanged() happens
mAdapter.setItems(it)
mPager.adapter = mAdapter // assign adapter to pager
mPager.currentItem = idx // finally set the current page
}
})
This will obviously do the correct order of operations without any hacks with Runnable or delays.
For the completeness, you usually implement the setItems() of the adapter (in this case FragmentStatePagerAdapter) like this:
internal fun setItems(items: List<Item>) {
this.items = items
notifyDataSetChanged()
}
I've used the post() method described here and sure enough it was working great under some scenarios but because my data comes from the server, it was not the holy grail.
My problem was that i want to have
notifyDataSetChanged
called at an arbitrary time and then switch tabs on my viewPager. So right after the notify call i have this
ViewUtilities.waitForLayout(myViewPager, new Runnable() {
#Override
public void run() {
myViewPager.setCurrentItem(tabIndex , false);
}
});
and
public final class ViewUtilities {
public static void waitForLayout(final View view, final Runnable runnable) {
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//noinspection deprecation
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
runnable.run();
}
});
}
}
Fun fact: the //noinspection deprecation at the end is because there is a spelling mistake in the API that was fixed after API 16, so that should read
removeOnGlobalLayoutListener
^^
ON Global
instead of
removeGlobalOnLayoutListener
^^
ON Layout
This seems to be covering all cases for me.
I was working on this problem for one week and I realized that this problem happens because I was using home activity context in view pager fragments and we can only use context in fragment after it gets attached to activity..
When a view pager gets created, activity only attach to the first (0) and second (1) page. When you open the second page, the third page gets attached and so on! When you use setCurrentItem() method and the argument is greater than 1, it wants to open that page before it is attached, so the context in fragment of that page will be null and the application gets crashed! That's why when you delay setCurrentItem(), it works! At first it gets attached and then it'll open the page...
This is a lifecycle issue, as pointed out by several posters here. However, I find the solutions with posting a Runnable to be unpredictable and probably error prone. It seems like a way to ignore the problem by posting it into the future.
I am not saying that this is the best solution, but it definitely works without using Runnable. I keep a separate integer inside the Fragment that has the ViewPager. This integer will hold the page we want to set as the current page when onResume is called next. The integer's value can be set at any point and can thus be set before a FragmentTransaction or when resuming an activity. Also note that all the members are set up in onResume(), not in onCreateView().
public class MyFragment extends Fragment
{
private ViewPager mViewPager;
private MyPagerAdapter mAdapter;
private TabLayout mTabLayout;
private int mCurrentItem = 0; // Used to keep the page we want to set in onResume().
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.my_layout, container, false);
mViewPager = (ViewPager) view.findViewById(R.id.my_viewpager);
mTabLayout = (TabLayout) view.findViewById(R.id.my_tablayout);
return view;
}
#Override
public void onResume()
{
super.onResume();
MyActivity myActivity = (MyActivity) getActivity();
myActivity.getSupportActionBar().setTitle(getString(R.string.my_title));
mAdapter = new MyPagerAdapter(getChildFragmentManager(), myActivity);
mViewPager.setAdapter(mAdapter);
mViewPager.setOffscreenPageLimit(PagerConstants.OFFSCREEN_PAGE_LIMIT);
mViewPager.setCurrentItem(mCurrentItem); // <-- Note the use of mCurrentItem here!
mTabLayout.setupWithViewPager(mViewPager);
}
/**
* Call this at any point before needed, for example before performing a FragmentTransaction.
*/
public void setCurrentItem(int currentItem)
{
mCurrentItem = currentItem;
// This should be called in cases where onResume() is not called later,
// for example if you only want to change the page in the ViewPager
// when clicking a Button or whatever. Just omit if not needed.
mViewPager.setCurrentItem(mCurrentItem);
}
}
For me this worked setting current item after setting adapter
viewPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager()));
viewPager.setCurrentItem(idx);
pagerSlidingTabStrip.setViewPager(viewPager);// assign viewpager to tabs
I've done it this way to restore the current item:
#Override
protected void onSaveInstanceState(Bundle outState) {
if (mViewPager != null) {
outState.putInt(STATE_PAGE_NO, mViewPager.getCurrentItem());
}
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mCurrentPage = savedInstanceState.getInt(STATE_PAGE_NO, 0);
}
super.onRestoreInstanceState(savedInstanceState);
}
#Override
protected void onRestart() {
mViewPager.setCurrentItem(mCurrentPage);
super.onRestart();
}
By the time I call setCurrentItem() the view is about to be recreated. So in fact I invoke setCurrentItem() for the viewpager and afterwards the system calls onCreateView() and hence creates a new viewpager.
This is the reason for me why I do not see any changes. And this is the reason why a postDelayed() may help.
Theoretical solution: Postpone the setCurrentItem() invocation until the view has been recreated.
Practical solution: I have no clue for a stable and simple solution. We should be able to check if the class is about to recreate it's view and if that is the case postpone the invocation of setCurrentItem() to the end of onCreateView()
I use the dsalaj code as a reference. If necessary I share the code with the complete solution.
I also strongly recommend using ViewPager2
Solution
Both cases have to go within the Observer {}:
First case: Initialize the adapter only when we have the first data set and not before, since this would generate inconsistencies in the paging. To the first data set we have to pass it as the argument of the Adapter.
Second case: From the first change in the observable we would have from the second data sets onwards which have to be passed to the Adapter through a public method only if we have already initialized the adapter with a first data set.
GL
I was confused with the onActivityCreated() getting invoked for unrelated tab #Mahdi Arabpour was an eye opener for me :)
For me the problem was the third page (as elaborated by #Mahdi Arabpour above) was getting reconstructed when I click the second tab, etc and it was losing its data adapter, setting it again in onActivityCreted solves my problems:
if (myXXRecyclerAdapter != null) {
myXXRecyclerAdapter = new MyXXRecyclerAdapter(myStoredData);
mRecyclerView.setAdapter(myXXRecyclerAdapter );
return;
}
You need to call pager.setCurrentItem(activePage) right after pager.setAdapter(buildAdapter())
#Override
public void onResume() {
if (pager.getAdapter() != null) {
activePage=pager.getCurrentItem();
Log.w(getClass().getSimpleName(), "pager.getAdapter()!=null");
pager.setAdapter(null);
}
pager.setAdapter(buildAdapter());
pager.setCurrentItem(activePage);
}
I have a Fragment (I'll call it pagerFragment) that is added to the backstack and is visible. It holds a viewPager with a FragmentPagerAdapter. The FragmentPagerAdapter holds (let's say) two fragments: A and B.
First adding of the fragments works great.
Fragment A has a button that once clicked, adds a fragment (C) to the backstack.
The problem is this: if I add that fragment (C), and then click back, the pagerAdapter is empty, and I cannot see any fragments inside.
If I use a hack, and destroy the children fragments (A and B) in the pagerFragments onDestroyView(), this solves the problem, although I don't wan't to use this hack.
Any ideas what the issue could be?
I had the same problem. The solution for me was simple:
in onCreateView I had:
// Create the adapter that will return a fragment for each of the three
// primary sections of the app.
mSectionsPagerAdapter = new SectionsPagerAdapter(getActivity()
.getSupportFragmentManager());
where SectionPageAdapter is something like this:
class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
after changing getSupportFragmentManager to
mSectionsPagerAdapter = new SectionsPagerAdapter(getChildFragmentManager());
it started working!
It sounds like you are using nested fragments since your ViewPager is inside a PagerFragment. Have you passed getChildFragmentManager() to the constructor of your FragmentPagerAdapter? If not you should.
I don't think you need a FragmentStatePagerAdapter, but I would give that a shot since it handles saving and restoring Fragment state. The fact that your onDestroyView() hack works makes me think that you may want a FragmentStatePagerAdapter.
It could also have something to do with the way the FragmentPagerAdapter adds Fragments. The FragmentPagerAdapter doesn't add Fragments to the backstack. Imagine if you had a 10+ pages added in your ViewPager and the user swiped through them. The user would need to hit back 11 times just to back out of the app.
It may also be related to this post: Nested Fragments and The Back Stack.
Also I'm not sure what you are adding the Fragment C to. Are you adding it to the same container as the ViewPager?
Well at least you have a few options to investigate. In these situations I like to debug down into the Android SDK source code and see what's causing the behaviour. I recommend grabbing the AOSP source and adding frameworks/support and frameworks/base as your SDK sources. That's the only true way to understand what is happening and avoid making random changes until things work.
Use getChildFragmentManager() instead of getSupportFragmentManager().
It will work fine.
I just faced the problem in our project as well. The root cause is the way the the FragmentPagerAdapter works:
The FragmentPagerAdapter just detaches a Fragment he does not currently need from its View but does not remove it from its FragmentManager. When he wants to display the Fragment again he looks if the FragmentManager still contains the Fragment using a tag that is created from the view id of the ViewPager and the id returned by the adapters getItemId(position) call. If he finds a Fragment he just schedules an attach of the Fragment to its View within the updating transaction of the FragmentManager. Only if he does not find a Fragment this way he creates a new one using the adapters getItem(position) call!
The problem with a Fragment containing a ViewPager with a FragmentPagerAdapter is, that the contents of the FragmentManager is never cleaned up when the containing Fragment is put to the back stack. If the containing Fragment comes back from the back stack it creates a new View but the FragmentManager still contains the fragments that were attached to the old view and the attach of an existing fragment does not work anymore.
The easiest way to get rid of this problem is to avoid nested fragments. :)
The second easiest way is as already mentioned in other posts to use the ChildFragmentManager for the FragmentPagerAdapter as this one gets properly updated during the life cycle of the container fragment.
As there are projects (as my current one) where both options are not possible, I have published here a solution that works with an arbitrary FragmentManager by using the hashCode of the sub fragments as the item id of the fragment at that position. It comes at the price of storing all fragments for all positions within the adapter.
public class MyPagerAdapter extends FragmentPagerAdapter {
private static int COUNT = ...;
private final FragmentManager fragmentManager;
private Fragment[] subFragments = new Fragment[COUNT];
private FragmentTransaction cleanupTransaction;
public MyPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
this.fragmentManager = fragmentManager;
}
#Override
public Fragment getItem(int position) {
return getSubFragmentAtPosition(position);
}
#Override
public int getCount() {
return COUNT;
}
#Override
public long getItemId(int position) {
return getSubFragmentAtPosition(position).hashCode();
}
//The next three methods are needed to remove fragments no longer used from the fragment manager
#Override
public void startUpdate(ViewGroup container) {
super.startUpdate(container);
cleanupTransaction = fragmentManager.beginTransaction();
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
cleanupTransaction.remove((Fragment) object);
}
#Override
public void finishUpdate(ViewGroup container) {
super.finishUpdate(container);
cleanupTransaction.commit();
}
private Fragment getSubFragmentAtPosition(int position){
if (subFragments[position] == null){
subFragments[position] = ...;
}
return subFragments[position];
}
}
I had same problem, just set adapter twice at once and that's all.
Example code :
private fun displayImg(photo1:String, photo2:String){
val pager:ViewPager = v?.findViewById(R.id.ProductImgPager)!!
val arr = ArrayList<String>()
arr.add(photo1)
arr.add(photo2)
pager.adapter = AdapterImageView(fm, arr ,arr.size)
pager.adapter = AdapterImageView(fm, arr ,arr.size)
}
I have a ViewPager with 3 Fragments and my FragmentPagerAdapter:
private class test_pager extends FragmentPagerAdapter {
public test_pager(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
return fragments[i];
}
#Override
public long getItemId(int position) {
if (position == 1) {
long res = fragments[position].hashCode()+fragment1_state.hashCode();
Log.d(TAG, "getItemId for position 1: "+res);
return res;
} else
return fragments[position].hashCode();
}
#Override
public int getCount() {
return fragments[2] == null ? 2 : 3;
}
#Override
public int getItemPosition(Object object) {
Fragment fragment = (Fragment) object;
for (int i=0; i<3; i++)
if (fragment.equals(fragments[i])){
if (i==1) {
return 1; // not sure if that makes a difference
}
return POSITION_UNCHANGED;
}
return POSITION_NONE;
}
}
In one of the page (#1), I keep changing the fragment to be displayed. The way I remove the old fragment is like this:
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(old_fragment1).commit();
And then just changing the value of fragments[1]
I found that I cannot really add or replace the new one or it will complain the ViewPager is trying to add it too with another tag... (am I doing something wrong here?)
All the fragments I display have setRetainInstance(true); in their onCreate function.
My problem is that this usually works well for the first few replacement, but then when I try to reuse a fragment, sometimes (I have not really figured out the pattern, the same fragment may be displayed several times before this happens) it will only show a blank page.
Here is what I have found happened in the callback functions of my Fragment I am trying to display when the problem happens:
onAttach is called (but at that time, getView is still null)
onCreateView is not called (that's expected)
onViewStateRestored is not called (why not?)
onResume is not called (I really thought it would...)
If it changes anything, I am using the support package, my activity is a SherlockFragmentActivity
EDIT (to answer Marco's comment):
The fragments are instantiated in the onCreate function of the Activity, I fill an ArrayList with those fragments:
char_tests = new ArrayList<Fragment>(Arrays.asList(
new FragmentOptionA(), new FragmentOptionB(), new FragmentOptionC()));
The I pick from that list to set fragments[1] (that's all done in the UI thread)
I fixed this by changing test_pager to extends FragmentStatePagerAdapter instead.
I am still confused as to what PagerAdapter should be used depending on the usage. The only thing I can find in the documentation says that FragmentPagerAdapter is better for smaller number of pages that would be kept in memory and FragmentPagerStateAdapter better for a larger number of pages where they would be destroyed and save memory...
When trying to do (fancy?) things with Fragments, I found FragmentStatePagerAdapter is better when pages are removed and re-inserted like in this case. And FragmentPagerAdapter is better when pages move position (see bug 37990)
I have a ViewPager (extends FragmentPagerAdapter) which holds two Fragments. What I need is just refresh a ListView for each Fragment when I swipe among them. For this I have implemented ViewPager.OnPageChangeListener interface (namely onPageScrollStateChanged). In order to hold references to Fragments I use a HashTable. I store references to Fragments in HashTable in getItem() method:
#Override
public Fragment getItem(int num) {
if (num == 0) {
Fragment itemsListFragment = new ItemsListFragment();
mPageReferenceMap.put(num, itemsListFragment);
return itemsListFragment;
} else {
Fragment favsListFragment = new ItemsFavsListFragment();
mPageReferenceMap.put(num, favsListFragment);
return favsListFragment;
}
}
So when I swipe from one Fragment to another the onPageScrollStateChanged triggers where I use the HashTable to call required method in both Fragments (refresh):
public void refreshList() {
((ItemsListFragment) mPageReferenceMap.get(0)).refresh();
((ItemsFavsListFragment) mPageReferenceMap.get(1)).refresh();
}
Everything goes fine until orientation change event happens. After it the code in refresh() method, which is:
public void refresh() {
mAdapter.changeCursor(mDbHelper.getAll());
getListView().setItemChecked(-1, true); // The last row from a exception trace finishes here (my class).
}
results in IllegalStateException:
java.lang.IllegalStateException: Content view not yet created
at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
at android.support.v4.app.ListFragment.getListView(ListFragment.java:222)
at ebeletskiy.gmail.com.passwords.ui.ItemsFavsListFragment.refresh(ItemsFavsListFragment.java:17)
Assuming the Content view is not created indeed I set the boolean variable in onActivityCreated() method to true and used if/else condition to call getListView() or not, which shown the activity and content view successfully created.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments. This is just my assumption.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments? May be some other solution? Thank you very much.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments.
The fragments should be automatically recreated, just like any fragment is on an configuration change. The exception would be if you used setRetainInstance(true), in which case they should be the same fragment objects as before.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments?
What is wrong with the fragments that are there?
I've spent some days searching for a solution for this problem, and many points was figured out:
use FragmentPagerAdapter instead of FragmentStatePagerAdapter
use FragmentStatePagerAdapter instead of FragmentPagerAdapter
return POSITION_NONE on getItemPosition override of FragmentPagerAdapter
don't use FragmentPagerAdapter if you need dynamic changes of Fragments
and many many many others...
In my app, like Eugene, I managed myself the instances of created fragments. I keep that in one HashMap<String,Fragment> inside some specialized class, so the fragments are never released, speeding up my app (but consuming more resources).
The problem was when I rotate my tablet (and phone). The getItem(int) wasn't called anymore for that fragment, and I couldn't change it.
I really spent many time until really found a solution, so I need share it with StackOverflow community, who helps me so many many times...
The solution for this problem, although the hard work to find it, is quite simple:
Just keep the reference to FragmentManager in the constructor of FragmentPagerAdapter extends:
public class Manager_Pager extends FragmentPagerAdapter {
private final FragmentManager mFragmentManager;
private final FragmentActivity mContext;
public Manager_Pager(FragmentActivity context) {
super( context.getSupportFragmentManager() );
this.mContext = context;
this.mFragmentManager = context.getSupportFragmentManager();
}
#Override
public int getItemPosition( Object object ) {
// here, check if this fragment is an instance of the
// ***FragmentClass_of_you_want_be_dynamic***
if (object instanceof FragmentClass_of_you_want_be_dynamic) {
// if true, remove from ***FragmentManager*** and return ***POSITION_NONE***
// to force a call to ***getItem***
mFragmentManager.beginTransaction().remove((Fragment) object).commit();
return POSITION_NONE;
}
//don't return POSITION_NONE, avoid fragment recreation.
return super.getItemPosition(object);
}
#Override
public Fragment getItem( int position ) {
if ( position == MY_DYNAMIC_FRAGMENT_INDEX){
Bundle args = new Bundle();
args.putString( "anything", position );
args.putString( "created_at", ALITEC.Utils.timeToStr() );
return Fragment.instantiate( mContext, FragmentClass_of_you_want_be_dynamic.class.getName(), args );
}else
if ( position == OTHER ){
//...
}else
return Fragment.instantiate( mContext, FragmentDefault.class.getName(), null );
}
}
Thats all. And it will work like a charm...
You can clear the saved instance state
protected void onCreate(Bundle savedInstanceState) {
clearBundle(savedInstanceState);
super.onCreate(savedInstanceState, R.layout.activity_car);
}
private void clearBundle(Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.remove("android:fragments");
savedInstanceState.remove("android:support:fragments");
savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
savedInstanceState.remove("android:lastAutofillId");
}
}