ViewPager retaing old Fragments on screen rotation - android

I am using ViewPager and FragmentStatePagerAdapter to show fragment. Initially there are 2 fragments added Fragment1 and Fragment2 and 3rd fragment Fragment3 is added at 2nd position after recieiving response from server. So after all pages are added this should be sequence of Fragments in ViewPager --> Fragment1, Fragment3, Fragment2.
The Problem is after Fragment1 and Fragment3 are added and app is making server call before adding 3rd Fragment at 2nd position if I do screen rotation multiple time then after 3rd fragment is added it still shows old copy of Fragment2 which was at 2nd position initially at 2nd position and 3rd position have new copy of Fragment2. So Fragment3 doest shows up in ViewPager. Sequence after adding 3rd Fragment -- > Fragment1, old copy of Fragment2, New Fragment2.
I am overriding onSaveInstanceState and calling super.onSaveInstanceState in my Activity.
Also I have tried returning POSITION_NONE from getItemPosition. I read somewhere that ViewPager save copies of fragment. Also through debugging I checked that ViewPager contained 2 copies of Fragment2 when issue was reproduced even though getItem of FragmentStatePagerAdapter returned different Fragments for each position but still at 2nd position it was showing old fragment. For testing purpose in getItem I returned Fragment1 for all positions so that all 3 pages should be same but even after that in 2nd position it was showing old copy of Fragment2 when I reproduced issue with steps mentioned above.
So how to clear ViewPager so that it does not save old fragments. How to refresh ViewPager so that it does not retain old copy with fragments. I think problem is with onSaveInstanceState but I need it. How can I exclude ViewPager when views are saved in onSaveInstanceState. I have tried mViewPager.setSaveEnabled(false) but it takes too much memory.
I found that the problem is instantiateItem method of adapter does not call getItem due to which old fragment is returned.
Below is solution code
Code:
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object obj = super.instantiateItem(container, position);
Fragment fragment = mFragments.get(position);
if((obj!=null && fragment!=null) && !(obj.getClass().getSimpleName().equals(fragment.clss.getSimpleName()))){
destroyItem(container, position, obj);
return super.instantiateItem(container, position);
}else{
return obj;
}
}

Only override this method in FragmentpagerAdapter
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
// TODO Auto-generated method stub
super.destroyItem(ViewGroup container, int position, Object object);
}
then remove super.destroyItem(ViewGroup container, int position, Object object);
from your code

I solved the problem, I am posting my solution here. I override instantiateItem of FragmentStatePagerAdapter and inside it I am getting object from super.instantiateItem and comparing its classname with classname of object at that position from list of fragments that I am maintaining.
If classname is not same its mean super.instantiateItem returns duplicate fragment. So I am calling destroyItem at that position and again calling super.instantiateItem and returning it from instantiateItem.
FragmentStatePagerAdapter return old fragment because in framework code arraylist mFragments has duplicate fragment and it just return fragment at current position. I think it should compare fragment at the position in list with currently displaying fragment before returning it.

Related

ViewPager with instant refresh android

I have a ViewPager and its child fragments are dynamic. I am using them in the same fragment class and I am changing the field's value dynamically. But when I changing the ViewPager position, It is not updating the fragment. I have to change the values instantly.
Thank you.
Declare refreshFragment method in your fragment class then
In ViewPagerAdapter class overwritte method
#Override
public int getItemPosition(#NonNull Object object) {
MyFragment f = (MyFragment ) object;
if (f != null) {
f.refreshFragment();
}
return super.getItemPosition(object);
}
When you call
viewPagerAdapter.notifyDataSetChanged();
it will call getItemPosition method in adapter class and update your fragment using refreshFragment method.
Android by default retains one page on both sides of the current page for optimised loading. Since you need the pages to refresh on every time, you need to use viewpager.setOffScreenPageLimit(0) to set all pages to be recreated every time they are viewed

ViewPager getItemPosition() never called. Who should call it?

I am a bit puzzled about the Fragment[State]PagerAdapter. Basically my issue is that I need to change the content (Fragments) of a ViewPager depending on screen orientation. I read a number of posts on this and some say I should override getItemPosition(Object) from PagerAdapter. I have done this but the method is never called, and I can't find any call to it in PagerAdapter, FragmentPagerAdapter nor FragmentStatePagerAdapter. Anyone out there know how this works and mind explaining?
Currently I made an ugly hack in my instantiateItem() to test a theory of mine, but this is nothing I really want to use. As you can see I remove the fragment if getItemPosition() return POSITION_NONE. In getItemPosition() I have some logic to determind if a fragment should still be in the pager or not.
#Override
public Object instantiateItem(ViewGroup container, int position) {
// Hack since getItemPosition is never called.
final long itemId = getItemId(position);
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if(fragment != null && getItemPosition(fragment) == POSITION_NONE) {
mFragmentManager.beginTransaction().remove(fragment).commit();
mFragmentManager.executePendingTransactions();
}
fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
JavaDoc says:
public int getItemPosition(Object object)
Called when the host view is attempting to determine if an item's position
has changed. Returns POSITION_UNCHANGED if the position of the given
item has not changed or POSITION_NONE if the item is no longer present
in the adapter.
The default implementation assumes that items will never
change position and always returns POSITION_UNCHANGED.
#param object Object representing an item, previously returned by a call to
instantiateItem(View, int).
#return object's new position index from [0, getCount()),
POSITION_UNCHANGED if the object's position has not changed,
POSITION_NONE if the item is no longer present.
I have tested my code on API level 19 and 22 and the issue is the same.
Thanks in advance.
EDIT: I extend FragmentStatePagerAdapter or FragmentPagerAdapter. If I extend PagerAdapter and wrote me own instantiate and destroy methods I might be able to workaround this. Was hoping I would not have too though. Plus it's annoying to not understand it. ;)
getItemPosition() is only called after a notifyDataSetChanged() call.
So first, modify your data set and then make a notifyDataSetChanged() call.
You will also have to override getItemId() to return a static identifier. By default it returns the position given.
getItemPosition() is called after notifyDataSetChanged() is called on the adapter. That's like saying you want to change which fragments are in the ViewPager.
For your case, if you're making changes on screen orientation, the entire activity is probably being taken down and rebuilt, so it's just a matter of having the adapter do the right thing. In very simplistic terms:
#Override
public Object instantiateItem(ViewGroup container, int position) {
if (orientation == ORIENTATION_PORTRAIT) {
// ... set up the views for portrait
} else {
// ... set up the views for landscape
}
}

Navigating back to FragmentPagerAdapter -> fragments are empty

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)
}

FragmentPagerAdapter getItem is not called

I am not able to reuse fragment in FragmentPagerAdapter.. Using destroyItem() method, It is deleting the fragment but still does not called getItem() again..There are just 2-3 Images so I am using FragmentPagerAdapter Instead of FragmentStatePagerAdapter..
public class ExamplePagerAdapter extends FragmentPagerAdapter {
ArrayList < String > urls;
int size = 0;
public ExamplePagerAdapter(FragmentManager fm, ArrayList < String > res) {
super(fm);
urls = res;
size = urls.size();
}
#Override
public int getCount() {
if (urls == null) {
return 0;
} else {
return size;
}
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
FragmentManager manager = ((Fragment) object).getFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove((Fragment) object);
trans.commit();
}
#Override
public Fragment getItem(int position) {
Fragment fragment = new FloorPlanFragment();
Bundle b = new Bundle();
b.putInt("p", position);
b.putString("image", urls.get(position));
Log.i("image", "" + urls.get(position));
fragment.setArguments(b);
return fragment;
}
}
And In FragmentActivity,
pager.setAdapter(new ExamplePagerAdapter(getSupportFragmentManager(), res2));
KISS Answer:
Simple use FragmentStatePagerAdapter instead of FragmentPagerAdapter.
I got the answer.. Firstly I thought to delete this question as I am doing a very silly mistake but this answer will help someone who is facing the same problem that Instead of FragmentPagerAdapter, use FragmentStatePagerAdapter.
As #BlackHatSamurai mentioned in the comment:
The reason this works is because FragmentStatePagerAdapter destroys
as Fragments that aren't being used. FragmentPagerAdapter does not.
Using a FragmentStatePagerAdapter didn't fully fix my problem which was a similar issue where onCreateView was not being called for child fragments in the view pager. I am actually nesting my FragmentPagerAdapter inside of another Fragment therefore the FragmentManager was shared throughout all of them and thus retaining instances of the old fragments. The fix was to instead feed an instance of the getChildFragmentManager to the constructor of the FragmentPagerAdapter in my host fragment. Something like...
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getChildFragmentManager());
The getChildFragmentManager() method is accessible via a fragment and this worked for me because it returns a private FragmentManager for that fragment specifically for situations in which nesting fragments is needed.
Keep in mind however to use getChildFragmentManager() your minimum API version must be atleast 17 (4.2), so this may throw a wrench in your gears. Of course, if you are using fragments from the support library v4 you should be okay.
Override long getItemId (int position)
FragmentPagerAdapter caches the fragments it creates using getItem. I was facing the same issue- even after calling notifyDataSetChanged() getItem was not being called.
This is actually a feature and not a bug. You need to override getItemId so that you can correctly reuse your fragments. Since you are removing fragments, your positions are changing. As mentioned in the docs:
long getItemId (int position)
Return a unique identifier for the item at the given position.
The default implementation returns the given position. Subclasses should override this method if the positions of items can change.
Just provide a unique id to each fragment and you're done.
Using a FragementStatePagerAdapter or returning POSITION_NONE in int getItemPosition (Object object) is wrong. You will not get any caching.
I did what #kanika and #Jraco11 had posted but I still had the problem.
So, after a lot of changes, I found one that worked for me and was added to my FragmentPagerAdapter the next code:
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
According to what I read, getItemPosition is used to notify the ViewPager whether or not to refresh an item, and to avoid updates if the items at the visible positions haven't changed.
method getItem() is used only to create new items. Once they created, this method will not be called. If you need to get item that is currently in use by adapter, use this method:
pagerAdapter.instantiateItem(viewPager, TAB_POS)
There are two different scenarios :
1.) You have same layout for every pager :
In that case, it will be better if you'll extend your custom adapter
by PagerAdapter and return a single layout.
2.) You have different layout for every pager :
In that case, it will be better if you'll extend your custom adapter
by FragmentStatePagerAdapter and return different fragmets for every pager.
I found that setting a listener on the tab-layout stopped this from being called, probably because they only have space for one listener on tabLayout.setOnTabSelectedListener instead of an array of listeners.

Replace a Fragment in a ViewPager using FragmentPagerAdapter

I am using a ViewPager with 4 pages, and I'm looking for an efficient way to replace/switch between fragments in each page.
This is the interaction pattern that I'm trying to create:
User presses a button on a page that currently holds fragment A
Fragment A is swapped out for some new fragment B
The user does some work in fragment B, and then presses a button when he/she is done
Fragment B is removed, and is replaced by fragment A (the original fragment)
I've found a way to do this, but it seems to have significant flaws. The solution involves removing the original fragment, and then overriding getItemPosition (essentially the method described in this related question):
//An array to keep track of the currently visible fragment in each page
private final Fragment[] activeFragments= new Fragment[4];
public void openFragmentB(ViewPager pager, int position) {
startUpdate(pager);
//Remove the original fragment
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.remove(activeFragments[position]);
transaction.commit();
//Create a new tile search fragment to replace the original fragment
activeFragments[position] = FragmentB.newInstance();
pageStates[position] = PageState.STATE_B;
finishUpdate(pager);
notifyDataSetChanged();
}
#Override
public int getItemPosition(Object object) {
//If the main fragment is not active, return POSITION_NONE
if(object instanceof FragmentA) {
FragmentA a = (FragmentA) object;
if(pageStates[a.getPosition()] != PageState.STATE_A) {
return POSITION_NONE;
}
}
//If the secondary fragment is not active, return POSITION_NONE
if(object instanceof FragmentB) {
FragmentB b = (FragmentB) object;
if(pageStates[b.getPosition()] != PageState.STATE_B) {
return POSITION_NONE;
}
}
return POSITION_UNCHANGED;
}
This method works, but has undesirable side effects. Removing the fragment and setting it's position to POSITION_NONE causes the fragment to be destroyed. So when the user finishes using FragmentB, I would need to create a new instance of FragmentA instead of reusing the original fragment. The main fragments in the pager (FragmentA in this example) will contain relatively large database backed lists, so I want to avoid recreating them if possible.
Essentially I just want to keep references to my 4 main fragments and swap them in and out of pages without having to recreate them every time. Any ideas?
A simple way to avoid recreating your Fragments is to keep them as member variables in your Activity. I do this anyway in conjunction with onRetainCustomNonConfigurationInstance() order to retain my fragments during configuration changes (mostly screen rotation). I keep my Fragments in a 'retainer' object since onRetainCustomNonConfigurationInstance only returns a single object.
In your case, instead of calling Fragment.newInstance() all the time, just check to see if the fragments contained in the retainer object is null before creating a new one. If it isn't null, just re-use the previous instance. This checking should happen in your ViewPager adapter's getItem(int) method.
In effect, doing this basically means you are handling whether or not Fragments are recycled when getItem is called, and overriding the getItemPosition(Object) method to always return POSITION_NONE when for relevant Segments.
FragmentPagerAdapter provides an overrideable method called getItemId that will help you here.
If you assign a unique long value to each Fragment in your collection, and return that in this method, it will force the ViewPager to reload a page when it notices the id has changed.
Better late than never, I hope this helps somebody out there!

Categories

Resources