Destroy item from the ViewPager's adapter after screen orientation changed - android

So I'm having a problem with destroying (removing) one page from the ViewPager after the screen orientation changed. I'll try to describe the problem in the following lines.
I'm using the FragmentStatePagerAdapter for the adapter of the ViewPager and a small interface which describes how an endless view pager should work. The idea behind of it is that you can scroll to the right till you reach the end of the ViewPager. If you can load more results from an API call, a progress page is displayed till the results come.
Everything fine till here, now the problem comes. If during this loading process, I rotate the screen (this will not affect the API call which is basically an AsyncTask), when the call returns, the app crashes giving me this exception:
E/AndroidRuntime(13471): java.lang.IllegalStateException: Fragment ProgressFragment{42b08548} is not currently in the FragmentManager
E/AndroidRuntime(13471): at android.support.v4.app.FragmentManagerImpl.saveFragmentInstanceState(FragmentManager.java:573)
E/AndroidRuntime(13471): at android.support.v4.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.java:136)
E/AndroidRuntime(13471): at mypackage.OutterFragment$PagedSingleDataAdapter.destroyItem(OutterFragment.java:609)
After digging a bit in the code of the library it seems that the mIndex data field of the fragment is less than 0 in this case, and this raises that exception.
Here is the code of the pager adapter:
static class PagedSingleDataAdapter extends FragmentStatePagerAdapter implements
IEndlessPagerAdapter {
private WeakReference<OutterFragment> fragment;
private List<DataItem> data;
private SparseArray<WeakReference<Fragment>> currentFragments = new SparseArray<WeakReference<Fragment>>();
private ProgressFragment progressElement;
private boolean isLoadingData;
public PagedSingleDataAdapter(SherlockFragment fragment, List<DataItem> data) {
super(fragment.getChildFragmentManager());
this.fragment = new WeakReference<OutterFragment>(
(OutterFragment) fragment);
this.data = data;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object item = super.instantiateItem(container, position);
currentFragments.append(position, new WeakReference<Fragment>(
(Fragment) item));
return item;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
currentFragments.put(position, null);
super.destroyItem(container, position, object);
}
#Override
public Fragment getItem(int position) {
if (isPositionOfProgressElement(position)) {
return getProgessElement();
}
WeakReference<Fragment> fragmentRef = currentFragments.get(position);
if (fragmentRef == null) {
return PageFragment.newInstance(args); // here I'm putting some info
// in the args, just deleted
// them now, not important
}
return fragmentRef.get();
}
#Override
public int getCount() {
int size = data.size();
return isLoadingData ? ++size : size;
}
#Override
public int getItemPosition(Object item) {
if (item.equals(progressElement) && !isLoadingData) {
return PagerAdapter.POSITION_NONE;
}
return PagerAdapter.POSITION_UNCHANGED;
}
public void setData(List<DataItem> data) {
this.data = data;
notifyDataSetChanged();
}
#Override
public boolean isPositionOfProgressElement(int position) {
return isLoadingData && position == data.size();
}
#Override
public void setLoadingData(boolean isLoadingData) {
this.isLoadingData = isLoadingData;
}
#Override
public boolean isLoadingData() {
return isLoadingData;
}
#Override
public Fragment getProgessElement() {
if (progressElement == null) {
progressElement = new ProgressFragment();
}
return progressElement;
}
public static class ProgressFragment extends SherlockFragment {
public ProgressFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
TextView progressView = new TextView(container.getContext());
progressView.setGravity(Gravity.CENTER_HORIZONTAL
| Gravity.CENTER_VERTICAL);
progressView.setText(R.string.loading_more_data);
LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT);
progressView.setLayoutParams(params);
return progressView;
}
}
}
The onPageSelected() callback below, which basically starts the api call if needed:
#Override
public void onPageSelected(int currentPosition) {
updatePagerIndicator(currentPosition);
activity.invalidateOptionsMenu();
if (requestNextApiPage(currentPosition)) {
pagerAdapter.setLoadingData(true);
requestNextPageController.requestNextPageOfData(this);
}
Now, it is also worth to say what the API call does after delivering the results. Here is the callback:
#Override
public boolean onTaskSuccess(Context arg0, List<DataItem> result) {
data = result;
pagerAdapter.setLoadingData(false);
pagerAdapter.setData(result);
activity.invalidateOptionsMenu();
return true;
}
Ok, now because the setData() method invokes the notifiyDataSetChanged(), this will call the getItemPosition() for the fragments that are currently in the currentFragments array. Of course that for the progress element it returns POSITION_NONE since I want to delete this page, so this basically invokes the destroyItem() callback from the PagedSingleDataAdapter. If I don't rotate the screen, everything works OK, but as I said if I'm rotating it when the progress element is displayed and the API call hasn't finished yet, the destroyItem() callback will be invoked after the activity is restarted.
Maybe I should also say that I'm hosting the ViewPager in another Fragment and not in an activity, so the OutterFragment hosts the ViewPager. I'm instantiating the pagerAdapter in the onActivityCreated() callback of the OutterFragment and using the setRetainInstance(true) so that when the screen rotates the pagerAdapter remains the same (nothing should be changed, right?), code here:
if (pagerAdapter == null) {
pagerAdapter = new PagedSingleDataAdapter(this, data);
}
pager.setAdapter(pagerAdapter);
if (savedInstanceState == null) {
pager.setOnPageChangeListener(this);
pager.setCurrentItem(currentPosition);
}
Summarizing now, the PROBLEM is:
If I try to remove the progress element from the ViewPager after it was instantiated and the activity was destroyed and recreated (screen orientation changed) I get the above exception (the pagerAdapter remains the same, so everything inside of it also remains the same, references etc… since the OutterFragment which hosts the pagerAdapter is not destroyed is only detached from the activity and then re-attached). Probably it happens something with the fragment manager, but I really don't know what.
What I've already tried:
Trying to remove my progress fragment using another technique i.e on the onTaskSuccess() callback I was trying to remove the fragment from the fragment manager, didn't work.
I also tried to hide the progress element instead of removing it completely from the fragment manager. This worked 50%, because the view was not there anymore, but I was having an empty page, so that's not really what I'm looking for.
I also tried to (re)attach the progressFragment to the fragment manager after the screen orientation changes, this also didn't work.
I also tried to remove and then add again the progress fragment to the fragment manager after the activity was recreated, didn't work.
Tried to call the destroyItem() manually from the onTaskSuccess() callback (which is really, really ugly) but didn't work.
Sorry guys for such a long post, but I was trying to explain the problem as best as I can so that you guys can understand it.
Any solution, recommendation is much appreciated.
Thanks!
UPDATE: SOLUTION FOUND
OK, so this took a while. The problem was that the destroyItem() callback was called twice on the progress fragment, once when the screen orientation changed and then once again after the api call finished. That's why the exception. The solution that I found is the following:
Keep tracking if the api call finished or not and destroy the progress fragment just in this case, code below.
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (object.equals(progressElement) && apiCallFinished == true) {
apiCallFinished = false;
currentFragments.put(position, currentFragments.get(position + 1));
super.destroyItem(container, position, object);
} else if (!(object.equals(progressElement))) {
currentFragments.put(position, null);
super.destroyItem(container, position, object);
}
}
and then this apiCallFinished is set to false in the constructor of the adapter and to true in the onTaskSuccess() callback.
And it really works!

UPDATE: SOLUTION FOUND OK, so this took a while. The problem was that the destroyItem() callback was called twice on the progress fragment, once when the screen orientation changed and then once again after the api call finished. That's why the exception. The solution that I found is the following: Keep tracking if the api call finished or not and destroy the progress fragment just in this case, code below.
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (object.equals(progressElement) && apiCallFinished == true) {
apiCallFinished = false;
currentFragments.put(position, currentFragments.get(position + 1));
super.destroyItem(container, position, object);
} else if (!(object.equals(progressElement))) {
currentFragments.put(position, null);
super.destroyItem(container, position, object);
}
}
and then this apiCallFinished is set to false in the constructor of the adapter and to true in the onTaskSuccess() callback. And it really works!

Related

Viewpager to update only current visible fragment

i update recycleView inside viewpager fragments and call pagerAdapter.notifyDataSetChanged(); to update them. It updates all recycleviews and the app takes long time. Now to overcome this problem i want to update only the current visible fragment and on ViewPage change update other fragments with notifyDataSetChanged(); is there a way to tell which fragment to update?
ViewPagerAdapter
private class MyPageAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragments;
private int[] mResources;
public MyPageAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.fragments = fragments;
}
#Override
public Fragment getItem(int position) {
return this.fragments.get(position);
}
#Override
public int getCount() {
return this.fragments.size();
}
#Override
public int getItemPosition(Object object) {
MyFragment f = (MyFragment) object;
if (f != null) {
f.update();
}
return super.getItemPosition(object);
}
}
this is fragment
public static class MyFragment extends Fragment implements CustomAdapterOdds.OnGameClickListener, Updateable {
public static final String COLUMN_IN_DISPLAY = "column_in_display";
RecyclerView recyclerView, recyclerView2;
HashMap<String, List<OddsFeed>> oddsList1;
HashMap<String, List<OddsFeed>> oddsList2;
int oddsColDisp;
CustomAdapterOdds adapterOdds1, adapterOdds2;
boolean isvisible = false;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
oddsList1 = new HashMap<>();
oddsList2 = new HashMap<>();
Bundle b = this.getArguments();
oddsColDisp = b.getInt(COLUMN_IN_DISPLAY);
List<GameFeed> leftGames = getArguments().getParcelableArrayList("list");
if (b.getSerializable("hashmap1") != null && b.getSerializable("hashmap2") != null) {
oddsList1 = (HashMap<String, List<OddsFeed>>) b.getSerializable("hashmap1");
oddsList2 = (HashMap<String, List<OddsFeed>>) b.getSerializable("hashmap2");
}
View v = inflater.inflate(R.layout.view_table_viewpager_fragment_layout, container, false);
ArrayList<GameFeed> gameFeedsCol0 = getArguments().getParcelableArrayList("list");
//recycleview1
recyclerView = v.findViewById(R.id.table_view_recycle_view_odds1);
adapterOdds1 = new CustomAdapterOdds(getContext(), leftGames, this, oddsList1, bestSitesList, oddsColDisp);
configRecyclerViewOdds(getContext(), recyclerView, adapterOdds1);
//recycleview2
recyclerView2 = v.findViewById(R.id.table_view_recycle_view_odds2);
adapterOdds2 = new CustomAdapterOdds(getContext(), leftGames, this, oddsList2, bestSitesList, oddsColDisp + 1, true);
configRecyclerViewOdds(getContext(), recyclerView2, adapterOdds2);
return v;
}
#Override
public void setMenuVisibility(final boolean visible) {
super.setMenuVisibility(visible);
if (visible) {
isvisible = true;
}
}
#Override
public void onResume() {
super.onResume();
Toast.makeText(getContext(), "onresume", Toast.LENGTH_SHORT).show();
adapterOdds1.updateListOdds(leftGames, oddsList1, oddsColDisp);
adapterOdds2.updateListOdds(leftGames, oddsList2, oddsColDisp + 1);
}
#Override
public void OnGameClickListener(View view, int position) {
}
#Override
public void update() {
Toast.makeText(getContext(), "update", Toast.LENGTH_SHORT).show();
if (isVisible()) {
adapterOdds1.updateListOdds(leftGames, oddsList1, oddsColDisp);
adapterOdds2.updateListOdds(leftGames, oddsList2, oddsColDisp + 1);
updateViewpager++;
int size = bestSitesList.size() / 2;
adapterOdds1.updateListOdds(leftGames, oddsList1, oddsColDisp);
adapterOdds2.updateListOdds(leftGames, oddsList2, oddsColDisp + 1);
if (updateViewpager == size - 1) {
progressBar.setVisibility(View.INVISIBLE);
updateViewpager = 0;
}
}
}
}
I'm sorry this is a bit of rambling answer but there was not enough code example given to make a functioning answer, so more really trying to explain a concept. Really need details of when and how the data is updated BUT...
As you are using androidx, you might want to consider moving to viewpager2 or with viewpager you can change how the fragment lifecycle states are managed.
If you changed to using BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT https://developer.android.com/reference/androidx/fragment/app/FragmentStatePagerAdapter#BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT when you constructed the Adapter https://developer.android.com/reference/androidx/fragment/app/FragmentStatePagerAdapter#FragmentStatePagerAdapter(androidx.fragment.app.FragmentManager,%20int)
Then all the Fragments will only be brought up to "Started" when created or re-created and only the current one on screen with "Resumed" and then "Paused" when moved off screen.
The exact mechanics depends on how the Fragments recyclerviews get their data and what triggers the update but then general idea is for the Fragments onCreateView to create a lightweight "shell" of a layout, I basically have the static buttons/text and create the recyclerview with an empty dataset.
Then in the onResume method of the Fragment which only gets called for the Fragment currently on Screen it calls your update method to replace the two recyclerview's empty datasets with the actual dataset and does a notifyDataSetChanged() on the recyclerviews.
Therefore when the viewpager is initially created X number of Fragments gets created and there static content is laid out plus 1 Fragment (the current one on screen) gets the recyclerview populated with actual data.
You might also then want to put in some optimisation checks in onResume of the Fragment to check the recyclerview views data has actually changed (a simple size check or using Recyclerview's DiffUtils) otherwise as you move between the Fragment's in the viewpager each Fragment will be paused/resumed.
This really only delays the cost of the two recyclerviews in the Fragment until it is really needed (when it is about to be displayed), it's a form of "Lazy loading"
With moving the "dynamic" data to this "lazy loading" is could be possible to remove the need to notifyDataSetChanged on the Fragments BUT the code snippets don't show enough about how and why the recyclerviews content changes.
With this method when the data is drawn is changed and you might not like how it looks.
This is really at a high level an inversion of the Fragments update logic, instead of saying "The data has changed redraw the data in all the Fragments" it is "This fragment is been shown, redraw the data IF it has been changed since the last time I drew it"

Android: View Pager of fragments that contain a scroll view, how to reset the scroll view to the top of the screen in OnPause

Background
FragmentList contains a view pager with an underlying FragmentStatePagerAdapter to show pages of FragmentDetail
FragmentDetail contains a scroll view
What i want to do
As you swipe, i want the previous FragmentDetail scroll position reset to the top. At the moment, when you swipe back to it, the scroll position goes back to where you left off.
E.g. I am on page 1 of view pager, i scroll to bottom of the current detail fragment. I then go to next page. Finally i go back to the first page, i want the scroll to be at the top and not where i left it
I tried the following
in OnPause of fragment detail, i tried the following code
#Override
public void onPause() {
super.onPause();
mScrollView.scrollTo(0,0);
}
I also tried the following
#Override
public void onPause() {
super.onPause();
mScrollView.post(new Runnable() {
#Override
public void run() {
mScrollView.fullScroll(View.FOCUS_UP);
}
});
}
Also put the scrollTo code in a runnable is well.
Does not scroll to the top
Forget about onPause(); you will drive yourself insane.
The best way I have found to do this sort of thing is override setPrimaryItem() in the FragmentPagerAdapter subclass. setPrimaryItem() is called whenever a fragment is displayed by the ViewPager.
Let's say you have an interface that looks like this:
public interface ResettableFragment {
public void reset();
}
and so your fragment implementation is:
#Override
public void reset() {
mScrollView.scrollTo(0,0);
}
In your FragmentPagerAdapter subclass, create a member
private ResettableFragment mLast;
and override setPrimaryItem() like this:
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (mLast != null) {
mLast.reset();
mLast = null;
}
if (object instanceof ResettableFragment) {
mLast = (ResettableFragment) object;
}
}
Some people prefer to register a ViewPager.OnPageChangListener which overrides onPageSelected() to do the reset logic instead of overriding setPrimaryItem() in the adapter.
That might look something like this:
viewPager.addOnPageChangeListener(new SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
if (position == POSITION_OF_PAGE_TO_RESET + 1 ||
position == POSITION_OF_PAGE_TO_RESET - 1) {
FragmentPagerAdapter adapter = (FragmentPagerAdapter) viewPager.getAdapter();
ResettableFragment fragment = (ResettableFragment) adapter.getItem(position);
fragment.reset();
}
}
});

Strange view pager behavior when swiping fast

I seem to be having a strange problem.
My ViewPager is working perfectly fine when I'm swiping it "slowly" (as in, there's a gap between my swipes - even half a second) but if I swipe fast enough (2-3 pages a second), after 3-4 pages, I observe one of these behaviors:
Sometimes it seems to jump back a couple of pages
Or simply show the same page again
Or show a white page (I imagine that's an empty page since my background is white)
Or pins itself between two pages (half of each is shown).
This is my adapter:
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
private SparseArray<String> _fragmentHeaders = new SparseArray<String>();
private SparseArray<Fragment> _fragments = new SparseArray<Fragment>();
private Integer _previousCount = null;
private boolean _isDatasetBeingChanged;
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
registerDataSetObserver(new DataSetObserver() {
#Override
public void onChanged() {
_previousCount = getContentCount();
super.onChanged();
}
});
}
public Fragment getExistingItem(int position) {
Fragment fragment = _fragments.get(position, null);
if (fragment != null) {
return fragment;
}
return null;
}
#Override
public Fragment getItem(int position) {
TType content = findContentWithPosition(position);
if (_fragmentHeaders.get(position, null) == null) {
_fragmentHeaders.put(position, content.getTitle());
}
Fragment fragment = createFragmentFromType(content);
_fragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
RoboFragment fragment = (RoboFragment) object;
if (fragment != null) {
fragment.onDestroy();
}
if (_fragmentHeaders.get(position, null) != null) {
_fragmentHeaders.remove(position);
}
if (_fragments.get(position, null) != null) {
_fragments.remove(position);
}
super.destroyItem(container, position, object);
}
#Override
public int getCount() {
int count = getContentCount();
if (_isDatasetBeingChanged) {
return count;
}
if (_previousCount != null && _previousCount != count) {
_isDatasetBeingChanged = true;
notifyDataSetChanged();
}
_previousCount = count;
_isDatasetBeingChanged = false;
return _previousCount;
}
#Override
public CharSequence getPageTitle(int position) {
return _fragmentHeaders.get(position);
}
}
which as I mentioned works if I swipe slowly. And I simply add the adapter like this:
_sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
_viewPager.setAdapter(_sectionsPagerAdapter);
Has anyone seen this before? I'm using rev 22 of the support libraries and everything is up to date in terms of the SDK.
I've tested this behavior on both 5.0 and 4.4 versions and both exhibit this problem.
Many thanks in advance,
p.s. the problem is not with calling notifyDataSetChanged(). Removing it had no effects and this happens even if the underlying page count has not changed.
Update 1:
I came across this Google page that seems to describe the problem I've mentioned here:
http://code.google.com/p/android-developer-preview/issues/detail?id=1190
The only difference is, with or without the CardView I get the same error so I don't think the problem is limited to the card view.
No resolution to this problem though so if anyone can help, I will be more than grateful.
I found out the problem after many hours of debugging into the source code of ViewPager. The issue was the my fragment contained a view that was also listening to touch events. When scrolling on a normal speed, the two would not interfere with each other (there's a Runnable() in the ViewPager I recommend having a look at and to understand its interaction with scrolling and also to understand how the velocity and "page jumping in VP operates) but when I was scrolling fact, the combination of the runnable and the velocity in the VP's onTouch would not play nicely with my view.
The solution was to remove the inner view that was listening to onTouch. While this was an extreme measure to take and made change some rather significant parts of my code and layout, I was simply too afraid to copy the entire VP's source code and change the part that was causing problems.
in your getItem method, do you mean to create a new fragment each time? what is _fragments? Instead try :
#Override
public Fragment getItem(int position) {
Fragment fragment = _fragments.get(position);
if (fragment == null) {
fragment = createFragmentFromType(content);
_fragments.put(position, fragment);
//save your fragment names, or better, create a custom fragment with a getTitle method.
}
return fragment;
}
for your getCount() method try:
#Override
public int getCount() {
return _fragments.size();
}
get rid of your override for destroyItem(), seems overly complicated and unnecessary.

ViewPager and removing pages

OK. I'm quite new to Android, and I have a problem with ViewPager and FragmentStatePagerAdapter. I have a chat application in which the chats are shown in the ViewPager. The chats can be closed dynamically and this works perfectly until I navigate out of my app, and return back to the chat activity (and onCreate of the activity is called again).
Let's say I have one chat window open. When I leave the activity, return later, and try to close the page, I get this exception:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 1, found: 0
Here's my FragmentStatePagerAdapter:
private class ChatPageAdapter extends FragmentStatePagerAdapter {
private Map<Integer, Fragment> pageReferenceMap = new HashMap<Integer, Fragment>();
private FragmentManager fragmentManager;
public ChatPageAdapter(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
public ChatFragment getItem(int position) {
Fragment fragment = ChatFragment.newInstance(Chat.getChat(position).getUniqueID());
pageReferenceMap.put(position, fragment);
return (ChatFragment) fragment;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
public int getCount() {
return Chat.getChatCount();
}
public Object instantiateItem(ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
pageReferenceMap.remove(position);
}
public ChatFragment getFragment(int position) {
return (ChatFragment) pageReferenceMap.get(position);
}
}
The chats are stored statically in the Chat-class, and getChatCount() simply returns the size of the ArrayList. I am not modifying the ArrayList in anywhere but two places, and these two places should be in sync with notifyDataSetChanged()... that is until the activity is recreated.
In my open(Chat) method I check if the Chat has already been "registered", and trigger notifyDataSetChanged only if it has not (to improve performance).
In my close(Chat) method I simply remove the Chat from the Chat-class, call notifyDataSetChanged on the adapter, and BOOM, the exception is thrown.
Is there somekind of culprit I should be aware of when the PageViewer and it's adapter is recreated? How do I keep the new adapter in sync with the already opened chats?
I've tried lots of things but nothing seems to work... I'm sure there's a simple solution to this problem.
Thanks a lot.
After ADT 22 the PagerAdapter has gotten very strict about calling notifyDataSetChanged() before calling getCount().
So the solution is simply to call notifyDataSetChanged() on the adapter every time the size of the data changes.
OK... I got this one eventually figured out. It was a stupid listener problem on my behalf. I forgot to unregister a listener from a chat when the activity was closed, and my onClosed(Chat) method got called twice.

Update ViewPager dynamically?

I can't update the content in ViewPager.
What is the correct usage of methods instantiateItem() and getItem() in FragmentPagerAdapter class?
I was using only getItem() to instantiate and return my fragments:
#Override
public Fragment getItem(int position) {
return new MyFragment(context, paramters);
}
This worked well. Except I can't change the content.
So I found this: ViewPager PagerAdapter not updating the View
"My approach is to use the setTag() method for any instantiated view in the instantiateItem() method"
Now I want to implement instantiateItem() to do that. But I don't know what I have to return (the type is Object) and what is the relation with getItem(int position)?
I read the reference:
public abstract Fragment getItem (int position)
Return the Fragment associated with a specified position.
public Object instantiateItem (ViewGroup container, int position)
Create the page for the given position. The adapter is responsible for adding the view to the container given here, although it only must ensure this is done by the time it returns from finishUpdate(ViewGroup).
Parameters
container The containing View in which the page will be shown.
position The page position to be instantiated.
Returns
Returns an Object representing the new page. This does not need to be a View, but can be some other container of the page.
but I still don't get it.
Here's my code. I'm using support package v4.
ViewPagerTest
public class ViewPagerTest extends FragmentActivity {
private ViewPager pager;
private MyFragmentAdapter adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pager1);
pager = (ViewPager)findViewById(R.id.slider);
String[] data = {"page1", "page2", "page3", "page4", "page5", "page6"};
adapter = new MyFragmentAdapter(getSupportFragmentManager(), 6, this, data);
pager.setAdapter(adapter);
((Button)findViewById(R.id.button)).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
reload();
}
});
}
private void reload() {
String[] data = {"changed1", "changed2", "changed3", "changed4", "changed5", "changed6"};
//adapter = new MyFragmentAdapter(getSupportFragmentManager(), 6, this, data);
adapter.setData(data);
adapter.notifyDataSetChanged();
pager.invalidate();
//pager.setCurrentItem(0);
}
}
MyFragmentAdapter
class MyFragmentAdapter extends FragmentPagerAdapter {
private int slideCount;
private Context context;
private String[] data;
public MyFragmentAdapter(FragmentManager fm, int slideCount, Context context, String[] data) {
super(fm);
this.slideCount = slideCount;
this.context = context;
this.data = data;
}
#Override
public Fragment getItem(int position) {
return new MyFragment(data[position], context);
}
#Override
public int getCount() {
return slideCount;
}
public void setData(String[] data) {
this.data = data;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
MyFragment
public final class MyFragment extends Fragment {
private String text;
public MyFragment(String text, Context context) {
this.text = text;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.slide, null);
((TextView)view.findViewById(R.id.text)).setText(text);
return view;
}
}
Here is also somebody with a similar problem, no answers
http://www.mail-archive.com/android-developers#googlegroups.com/msg200477.html
When using FragmentPagerAdapter or FragmentStatePagerAdapter, it is best to deal solely with getItem() and not touch instantiateItem() at all. The instantiateItem()-destroyItem()-isViewFromObject() interface on PagerAdapter is a lower-level interface that FragmentPagerAdapter uses to implement the much simpler getItem() interface.
Before getting into this, I should clarify that
if you want to switch out the actual fragments that are being displayed, you need to avoid FragmentPagerAdapter and use
FragmentStatePagerAdapter.
An earlier version of this answer made the mistake of using FragmentPagerAdapter for its example - that won't work because FragmentPagerAdapter never destroys a fragment after it's been displayed the first time.
I don't recommend the setTag() and findViewWithTag() workaround provided in the post you linked. As you've discovered, using setTag() and findViewWithTag() doesn't work with fragments, so it's not a good match.
The right solution is to override getItemPosition(). When notifyDataSetChanged() is called, ViewPager calls getItemPosition() on all the items in its adapter to see whether they need to be moved to a different position or removed.
By default, getItemPosition() returns POSITION_UNCHANGED, which means, "This object is fine where it is, don't destroy or remove it." Returning POSITION_NONE fixes the problem by instead saying, "This object is no longer an item I'm displaying, remove it." So it has the effect of removing and recreating every single item in your adapter.
This is a completely legitimate fix! This fix makes notifyDataSetChanged behave like a regular Adapter without view recycling. If you implement this fix and performance is satisfactory, you're off to the races. Job done.
If you need better performance, you can use a fancier getItemPosition() implementation. Here's an example for a pager creating fragments off of a list of strings:
ViewPager pager = /* get my ViewPager */;
// assume this actually has stuff in it
final ArrayList<String> titles = new ArrayList<String>();
FragmentManager fm = getSupportFragmentManager();
pager.setAdapter(new FragmentStatePagerAdapter(fm) {
public int getCount() {
return titles.size();
}
public Fragment getItem(int position) {
MyFragment fragment = new MyFragment();
fragment.setTitle(titles.get(position));
return fragment;
}
public int getItemPosition(Object item) {
MyFragment fragment = (MyFragment)item;
String title = fragment.getTitle();
int position = titles.indexOf(title);
if (position >= 0) {
return position;
} else {
return POSITION_NONE;
}
}
});
With this implementation, only fragments displaying new titles will get displayed. Any fragments displaying titles that are still in the list will instead be moved around to their new position in the list, and fragments with titles that are no longer in the list at all will be destroyed.
What if the fragment has not been recreated, but needs to be updated anyway? Updates to a living fragment are best handled by the fragment itself. That's the advantage of having a fragment, after all - it is its own controller. A fragment can add a listener or an observer to another object in onCreate(), and then remove it in onDestroy(), thus managing the updates itself. You don't have to put all the update code inside getItem() like you do in an adapter for a ListView or other AdapterView types.
One last thing - just because FragmentPagerAdapter doesn't destroy a fragment doesn't mean that getItemPosition is completely useless in a FragmentPagerAdapter. You can still use this callback to reorder your fragments in the ViewPager. It will never remove them completely from the FragmentManager, though.
Instead of returning POSITION_NONE from getItemPosition() and causing full view recreation, do this:
//call this method to update fragments in ViewPager dynamically
public void update(UpdateData xyzData) {
this.updateData = xyzData;
notifyDataSetChanged();
}
#Override
public int getItemPosition(Object object) {
if (object instanceof UpdateableFragment) {
((UpdateableFragment) object).update(updateData);
}
//don't return POSITION_NONE, avoid fragment recreation.
return super.getItemPosition(object);
}
Your fragments should implement UpdateableFragment interface:
public class SomeFragment extends Fragment implements
UpdateableFragment{
#Override
public void update(UpdateData xyzData) {
// this method will be called for every fragment in viewpager
// so check if update is for this fragment
if(forMe(xyzData)) {
// do whatever you want to update your UI
}
}
}
and the interface:
public interface UpdateableFragment {
public void update(UpdateData xyzData);
}
Your data class:
public class UpdateData {
//whatever you want here
}
for those who still face the same problem which i faced before when i have a ViewPager with 7 fragments. the default for these fragments to load the English content from API service but the problem here that i want to change the language from settings activity and after finish
settings activity i want ViewPager in main activity to refresh the fragments to match the language selection from the user and load the Arabic content if user chooses Arabic here what i did to work from the first time
1- You must use FragmentStatePagerAdapter as mentioned above.
2- on mainActivity i override the onResume and did the following
if (!(mPagerAdapter == null)) {
mPagerAdapter.notifyDataSetChanged();
}
3-i overrided the getItemPosition() in mPagerAdapter and make it return POSITION_NONE.
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
works like charm
I have encountered this problem and finally solved it today, so I write down what I have learned and I hope it is helpful for someone who is new to Android's ViewPager and update as I do. I'm using FragmentStatePagerAdapter in API level 17 and currently have just 2 fragments. I think there must be something not correct, please correct me, thanks.
Serialized data has to be loaded into memory. This can be done using a CursorLoader/AsyncTask/Thread. Whether it's automatically loaded depends on your code. If you are using a CursorLoader, it's auto-loaded since there is a registered data observer.
After you call viewpager.setAdapter(pageradapter), the adapter's getCount() is constantly called to build fragments. So if data is being loaded, getCount() can return 0, thus you don't need to create dummy fragments for no data shown.
After the data is loaded, the adapter will not build fragments automatically since getCount() is still 0, so we can set the actually loaded data number to be returned by getCount(), then call the adapter's notifyDataSetChanged(). ViewPager begin to create fragments (just the first 2 fragments) by data in memory. It's done before notifyDataSetChanged() is returned. Then the ViewPager has the right fragments you need.
If the data in the database and memory are both updated (write through), or just data in memory is updated (write back), or only data in the database is updated. In the last two cases if data is not automatically loaded from the database to memory (as mentioned above).
The ViewPager and pager adapter just deal with data in memory.
So when data in memory is updated, we just need to call the adapter's notifyDataSetChanged(). Since the fragment is already created, the adapter's onItemPosition() will be called before notifyDataSetChanged() returns. Nothing needs to be done in getItemPosition(). Then the data is updated.
Try destroyDrawingCache() on ViewPager after notifyDataSetChanged() in your code.
After hours of frustration while trying all the above solutions to overcome this problem and also trying many solutions on other similar questions like this, this and this which all FAILED with me to solve this problem and to make the ViewPager to destroy the old Fragment and fill the pager with the new Fragments. I have solved the problem as following:
1) Make the ViewPager class to extends FragmentPagerAdapter as following:
public class myPagerAdapter extends FragmentPagerAdapter {
2) Create an Item for the ViewPager that store the title and the fragment as following:
public class PagerItem {
private String mTitle;
private Fragment mFragment;
public PagerItem(String mTitle, Fragment mFragment) {
this.mTitle = mTitle;
this.mFragment = mFragment;
}
public String getTitle() {
return mTitle;
}
public Fragment getFragment() {
return mFragment;
}
public void setTitle(String mTitle) {
this.mTitle = mTitle;
}
public void setFragment(Fragment mFragment) {
this.mFragment = mFragment;
}
}
3) Make the constructor of the ViewPager take my FragmentManager instance to store it in my class as following:
private FragmentManager mFragmentManager;
private ArrayList<PagerItem> mPagerItems;
public MyPagerAdapter(FragmentManager fragmentManager, ArrayList<PagerItem> pagerItems) {
super(fragmentManager);
mFragmentManager = fragmentManager;
mPagerItems = pagerItems;
}
4) Create a method to re-set the adapter data with the new data by deleting all the previous fragment from the fragmentManager itself directly to make the adapter to set the new fragment from the new list again as following:
public void setPagerItems(ArrayList<PagerItem> pagerItems) {
if (mPagerItems != null)
for (int i = 0; i < mPagerItems.size(); i++) {
mFragmentManager.beginTransaction().remove(mPagerItems.get(i).getFragment()).commit();
}
mPagerItems = pagerItems;
}
5) From the container Activity or Fragment do not re-initialize the adapter with the new data. Set the new data through the method setPagerItems with the new data as following:
ArrayList<PagerItem> pagerItems = new ArrayList<PagerItem>();
pagerItems.add(new PagerItem("Fragment1", new MyFragment1()));
pagerItems.add(new PagerItem("Fragment2", new MyFragment2()));
mPagerAdapter.setPagerItems(pagerItems);
mPagerAdapter.notifyDataSetChanged();
I hope it helps.
For some reason none of the answers worked for me so I had to override the restoreState method without calling super in my fragmentStatePagerAdapter. Code:
private class MyAdapter extends FragmentStatePagerAdapter {
// [Rest of implementation]
#Override
public void restoreState(Parcelable state, ClassLoader loader) {}
}
I slightly modified the solution provided by Bill Phillips to suit my needs
private class PagerAdapter extends FragmentStatePagerAdapter{
Bundle oBundle;
FragmentManager oFragmentManager;
ArrayList<Fragment> oPooledFragments;
public PagerAdapter(FragmentManager fm) {
super(fm);
oFragmentManager=fm;
}
#Override
public int getItemPosition(Object object) {
Fragment oFragment=(Fragment)object;
oPooledFragments=new ArrayList<>(oFragmentManager.getFragments());
if(oPooledFragments.contains(oFragment))
return POSITION_NONE;
else
return POSITION_UNCHANGED;
}
}
so that the getItemPosition() returns POSITION_NONE only for those fragments which are currently in the FragmentManager when getItemPosition is called.
(Note that this FragmentStatePager and the ViewPager associated with it are contained in a Fragment not in a Activity)
I had a similar problem but don't want to trust on the existing solutions (hard coded tag names etc.) and I couldn't make M-WaJeEh's solution work for me. Here is my solution:
I keep references to the fragments created in getItem in an array. This works fine as long as the activity is not destroyed due to configurationChange or lack of memory or whatever (--> when coming back to the activity, fragments return to their last state without 'getItem' being called again and thus without updating the array).
To avoid this problem I implemented instantiateItem(ViewGroup, int) and update my array there, like this:
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object o = super.instantiateItem(container, position);
if(o instanceof FragmentX){
myFragments[0] = (FragmentX)o;
}else if(o instanceof FragmentY){
myFragments[1] = (FragmentY)o;
}else if(o instanceof FragmentZ){
myFragments[2] = (FragmentZ)o;
}
return o;
}
So, on the one hand I'm happy that I found a solution that works for me and wanted to share it with you, but I also wanted to ask whether somebody else tried something similar and whether there is any reason why I shouldn't do it like that? So far it works very good for me...
I have lived same problem and I have searched too much times. Any answer given in stackoverflow or via google was not solution for my problem. My problem was easy. I have a list, I show this list with viewpager. When I add a new element to head of the list and I refresh the viewpager nothings changed. My final solution was very easy anybody can use. When a new element added to list and want to refresh the list. Firstly set viewpager adapter to null then recreate the adapter and set i to it to viewpager.
myVPager.setAdapter(null);
myFragmentAdapter = new MyFragmentAdapter(getSupportFragmentManager(),newList);
myVPager.setAdapter(myFragmentAdapter);
Be sure your adapter must extend FragmentStatePagerAdapter
I use EventBus library to update Fragment content in ViewPager. The logic is simple, just like document of EventBus how to do. It is no need to control FragmentPagerAdapter instance. The code is here:
1: Define events
Define which message which is needed to update.
public class UpdateCountEvent {
public final int count;
public UpdateCountEvent(int count) {
this.count = count;
}
}
2.Prepare subscribers
Write below code in the Fragment which is needed update.
#Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
#Override
public void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
public void onEvent(UpdateCountEvent event) {//get update message
Toast.makeText(getActivity(), event.count, Toast.LENGTH_SHORT).show();
}
3.Post events
Write below code in other Activity or other Fragment which needs to update parameter
//input update message
EventBus.getDefault().post(new UpdateCountEvent(count));
I had been trying so many different approaches, none really sove my problem. Below are how I solve it with a mix of solutions provided by you all. Thanks everyone.
class PagerAdapter extends FragmentPagerAdapter {
public boolean flag_refresh=false;
public PagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int page) {
FragmentsMain f;
f=new FragmentsMain();
f.page=page;
return f;
}
#Override
public int getCount() {
return 4;
}
#Override
public int getItemPosition(Object item) {
int page= ((FragmentsMain)item).page;
if (page == 0 && flag_refresh) {
flag_refresh=false;
return POSITION_NONE;
} else {
return super.getItemPosition(item);
}
}
#Override
public void destroyItem(View container, int position, Object object) {
((ViewPager) container).removeView((View) object);
}
}
I only want to refresh page 0 after onResume().
adapter=new PagerAdapter(getSupportFragmentManager());
pager.setAdapter(adapter);
#Override
protected void onResume() {
super.onResume();
if (adapter!=null) {
adapter.flag_refresh=true;
adapter.notifyDataSetChanged();
}
}
In my FragmentsMain, there is public integer "page", which can tell me whether it is the page I want to refresh.
public class FragmentsMain extends Fragment {
private Cursor cursor;
private static Context context;
public int page=-1;
I know am late for the Party. I've fixed the problem by calling TabLayout#setupWithViewPager(myViewPager); just after FragmentPagerAdapter#notifyDataSetChanged();
If you want to use FragmentStatePagerAdapter, please take a look at https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars&groupby=&sort=&id=37990.
There are issues with FragmentStatePagerAdapter that may or may not trouble your use case.
Also, link has few solutions too..few may suit to your requirement.
This might be of help to someone - in my case when inserting a new page the view pager was asking for the position of an existing fragment twice, but not asking for the position of the new item, causing incorrect behaviour and data not displaying.
Copy the source for for FragmentStatePagerAdapter (seems to have not been updated in ages).
Override notifyDataSetChanged()
#Override
public void notifyDataSetChanged() {
mFragments.clear();
super.notifyDataSetChanged();
}
Add a sanity check to destroyItem() to prevent crashes:
if (position < mFragments.size()) {
mFragments.set(position, null);
}
Here is my implementation that incorporates the info from #Bill Phillips
One gets fragment caching most of the time, except when the data has changed. Simple, and seems to work fine.
MyFragmentStatePagerAdapter.java
private boolean mIsUpdating = false;
public void setIsUpdating(boolean mIsUpdating) {
this.mIsUpdating = mIsUpdating;
}
#Override
public int getItemPosition(#NonNull Object object) {
if (mIsUpdating) {
return POSITION_NONE;
}
else {
return super.getItemPosition(object);
}
}
MyActivity.java
mAdapter.setIsUpdating(true);
mAdapter.notifyDataSetChanged();
mAdapter.setIsUpdating(false);
Using ViewPager2 and FragmentStateAdapter:
Updating data dynamically is supported by ViewPager2.
There is an important note in the docs on how to get this working:
Note: The DiffUtil utility class relies on identifying items by ID. If you are using ViewPager2 to page through a mutable collection, you must also override getItemId() and containsItem(). (emphasis mine)
Based on ViewPager2 documentation and Android's Github sample project there are a few steps we need to take:
Set up FragmentStateAdapter and override the following methods: getItemCount, createFragment, getItemId, and containsItem (note: FragmentStatePagerAdapter is not supported by ViewPager2)
Attach adapter to ViewPager2
Dispatch list updates to ViewPager2 with DiffUtil (don't need to use DiffUtil, as seen in sample project)
Example:
private val items: List<Int>
get() = viewModel.items
private val viewPager: ViewPager2 = binding.viewPager
private val adapter = object : FragmentStateAdapter(this#Fragment) {
override fun getItemCount() = items.size
override fun createFragment(position: Int): Fragment = MyFragment()
override fun getItemId(position: Int): Long = items[position].id
override fun containsItem(itemId: Long): Boolean = items.any { it.id == itemId }
}
viewPager.adapter = adapter
private fun onDataChanged() {
DiffUtil
.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int = viewPager.adapter.itemCount
override fun getNewListSize(): Int = viewModel.items.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
viewPager.adapter?.getItemId(oldItemPosition) == viewModel.items[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areItemsTheSame(oldItemPosition, newItemPosition)
}, false)
.dispatchUpdatesTo(viewPager.adapter!!)
}
This solution won't work for everyone, but in my case, every Fragment in my ViewPager is a different class, and only one of them ever exist at a time.
With this constraint, this solution is safe and should be safe to use in production.
private void updateFragment(Item item) {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
for (Fragment fragment : fragments) {
if (fragment instanceof MyItemFragment && fragment.isVisible()) {
((MyItemFragment) fragment).update(item);
}
}
}
If you have multiple versions of the same fragment, you can use this same strategy to call methods on those fragments to determine if it is the fragment you wish to update.
I've gone through all the answers above and a number of others posts but still couldn't find something that worked for me (with different fragment types along with dynamically adding and removing tabs). FWIW following approach is what worked for me (in case anyone else has same issues).
public class MyFragmentStatePageAdapter extends FragmentStatePagerAdapter {
private static final String TAB1_TITLE = "Tab 1";
private static final String TAB2_TITLE = "Tab 2";
private static final String TAB3_TITLE = "Tab 3";
private ArrayList<String> titles = new ArrayList<>();
private Map<Fragment, Integer> fragmentPositions = new HashMap<>();
public MyFragmentStatePageAdapter(FragmentManager fm) {
super(fm);
}
public void update(boolean showTab1, boolean showTab2, boolean showTab3) {
titles.clear();
if (showTab1) {
titles.add(TAB1_TITLE);
}
if (showTab2) {
titles.add(TAB2_TITLE);
}
if (showTab3) {
titles.add(TAB3_TITLE);
}
notifyDataSetChanged();
}
#Override
public int getCount() {
return titles.size();
}
#Override
public Fragment getItem(int position) {
Fragment fragment = null;
String tabName = titles.get(position);
if (tabName.equals(TAB1_TITLE)) {
fragment = Tab1Fragment.newInstance();
} else if (tabName.equals(TAB2_TITLE)) {
fragment = Tab2Fragment.newInstance();
} else if (tabName.equals(TAB3_TITLE)) {
fragment = Tab3Fragmen.newInstance();
}
((BaseFragment)fragment).setTitle(tabName);
fragmentPositions.put(fragment, position);
return fragment;
}
#Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
#Override
public int getItemPosition(Object item) {
BaseFragment fragment = (BaseFragment)item;
String title = fragment.getTitle();
int position = titles.indexOf(title);
Integer fragmentPosition = fragmentPositions.get(item);
if (fragmentPosition != null && position == fragmentPosition) {
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
fragmentPositions.remove(object);
}
}
Use FragmentStatePagerAdapter instead of FragmentPagerAdapter
if you want to recreate or reload fragment on index basis
For example if you want to reload fragment other than FirstFragment, you can check instance and return position like this
public int getItemPosition(Object item) {
if(item instanceof FirstFragment){
return 0;
}
return POSITION_NONE;
}
You need change instantiateItem's mFragments element getItemPosition.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
int newPosition = getItemPosition(f);
if (newPosition == POSITION_UNCHANGED) {
return f;
} else if (newPosition == POSITION_NONE) {
mFragments.set(position, null);
} else {
mFragments.set(newPosition, f);
}
}
}
Based AndroidX FragmentStatePagerAdapter.java, because mFragments's elements position do not change when calling notifyDataSetChanged().
Source:
https://github.com/cuichanghao/infivt/blob/master/library/src/main/java/cc/cuichanghao/library/FragmentStatePagerChangeableAdapter.java
Example:
https://github.com/cuichanghao/infivt/blob/master/app/src/main/java/cc/cuichanghao/infivt/MainActivityChangeablePager.kt
You can run this project to confirm how to work.
https://github.com/cuichanghao/infivt

Categories

Resources