Android ViewPager - access view inside the current page throws NPE - android

I'm using android.support.v4.view.ViewPager and noticed, that after setting my android.support.v4.app.FragmentStatePagerAdapter on the ViewPager in the onStart()-method of my activity I can't retrieve a view of the currently displayed ViewPager page, because it doesn't seem to be initialized/attached/drawn to the activity yet. I always get a NPE.
Is there an event which is fired, when my ViewPager is fully initialized including its fragment pages and their views?
My code in the onStart()-method of the main activity looks something like this:
ViewPager vp = findViewById(R.id.viewPager);
vp.setAdapter(new FlashCardPagerAdapter(getSupportFragmentManager()));
vp.getChildCount(); //this is still 0 because the child fragments have not been added yet, why? When are they added?
My Adapter looks like this:
public class FlashCardPagerAdapter extends FragmentStatePagerAdapter {
private List<FlashCard> flashCards;
private FlashCardFragment[] flashCardFragments;
public FlashCardPagerAdapter(FragmentManager fm) {
super(fm);
flashCards = FlashCardApp.getInstance().getFlashCards();
flashCardFragments = new FlashCardFragment[flashCards.size()];
}
#Override
public Fragment getItem(int i) {
if (flashCardFragments[i] != null) {
return flashCardFragments[i];
} else {
flashCardFragments[i] = new FlashCardFragment(flashCards.get(i));
return flashCardFragments[i];
}
}
public FlashCardView getFlashCardViewAt(int i) {
return ((FlashCardFragment) getItem(i)).getFlashCardView();
}
#Override
public int getCount() {
return 25;
}
}
Thanks for any help.
EDIT
Thing is, all this works when I call it after the onStart() method, for example in one of my custom button views.

I'm guessing vp.setAdapter(new MyAdapter()); is just a typo and you meant to say vp.setAdapter(new FlashCardPagerAdapter(getSupportFragmentManager()));
If that doesn't work, then it's returning 0 pages for some reason. I would check the adapter's getCount() method to make sure it's returning something more than 0, because that's how the viewpager decides how many pages to show.

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"

ViewPager holds fragment's instance even after setting a new adapter

I'm facing some issues with ViewPager and Fragment's instances.
I have a ViewPager (let's call Father) with 4 fragments and into the last fragment I have another ViewPager (and call it Child) with dynamic fragments amount. What I mean is that I create the Child based on a list of objects in memory. So if the list contains 3 objets, the Child will have 3 fragments inside. If in a determinate moment something happens and I get a list with 1 object, then I must update the Child with just 1 fragment. An important point in here is that each Child'sFragment has its own object from the returned list and is created based on this object.
The code I do to set the list of fragments into the Child ViewPager is the following:
#Override
public void setViewPagerChildFragments(List<Fragment> fragments) {
if (fragments != null) {
DefaultStateViewPagerAdapter adapter = (DefaultStateViewPagerAdapter) mViewpagerChild.getAdapter();
if (adapter == null) {
/* In this case I use getChildFragmentManager() because
it's inside the last fragment of the ViewPager Father*/
adapter = new DefaultStateViewPagerAdapter(getChildFragmentManager(), fragments);
mViewpagerChild.setAdapter(adapter);
} else {
adapter.setFragments(fragments); // Into this method I do notifyDataSetChanged() already
}
}
}
Note that I try to use the same adapter's instance to set the fragments and then notify the changes (notifyDataSetChanged() is inside the method). If I don't get the adapter's instance, I create a new one and set it to the ViewPager Child.
The problem happens, for example, when I set the ViewPager Child with 2 fragments and after a while I need to set it with 1 fragment. The ViewPager shows just 1 fragment inside it, but the second one is still attached and isn't destroyed. I know it because I did a test calling getChildFragmentManager().getFragments(), and I could see the fragment which was supposed to be destroyed is still there.
You may say it isn't actually a problem, since the Garbage Collector can remove the unused Fragment. However, if in some moment for example, I try to set 2 new fragments again into the ViewPager Child, it uses that unused Fragment instance instead of creating a new one and unfortunately it also uses its same object, and not the right new one.
This is my DefaultStateViewPagerAdaptercode:
public class DefaultStateViewPagerAdapter extends FragmentStatePagerAdapter {
private ArrayList<Fragment> mFragments;
private ArrayList<String> mFragmentTitles;
public DefaultStateViewPagerAdapter(FragmentManager fm) {
super(fm);
this.mFragments = new ArrayList<>();
this.mFragmentTitles = new ArrayList<>();
}
public DefaultStateViewPagerAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.mFragments = (ArrayList<Fragment>) fragments;
}
#Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
#Override
public int getItemPosition(Object object) {
int index = mFragments.indexOf(object);
if (index < 0) {
index = POSITION_NONE;
}
return index;
}
#Override
public int getCount() {
return mFragments.size();
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitles.get(position);
}
public void clearFragments() {
mFragments.clear();
mFragmentTitles.clear();
}
public void setFragments(List<Fragment> fragments) {
mFragments = (ArrayList<Fragment>) fragments;
notifyDataSetChanged();
}
public void addFragment(Fragment fragment, String title) {
mFragments.add(fragment);
mFragmentTitles.add(title);
}
}
I already tried to override saveState in order to avoid the ViewPager uses the old fragment´s instance, like:
#Override
public Parcelable saveState() {
return null;
}
It worked, and the ViewPager no longer uses the old reference, but the unused Fragment is still attached, and it causes memory leak.
I don't know why the ViewPager doesn't destroy its fragments even after I set a new adapter. Has anyone ever had this issue?
I found the solution.
The solution is very simple. I just had to set manually null to the Child's adapter. With this, the ViewPager is forced to destroy every fragment.
So into the onDestroyView of Fragment's father I added:
#Override
public void onDestroyView() {
super.onDestroyView();
mViewpagerChild.removeOnPageChangeListener(mOnPaymentMethodsPageChangeListener);
mViewpagerChild.setAdapter(null); // <-- This is what I added
}

Updating fragments/views in viewpager with fragmentStatePagerAdapter

Need some help with my problem of updating pages while using viewpager. I am using a simple viewpager with FragmentStatePagerAdapter. All I want to do is get access to the current fragment/view so as I can update some textviews in my fragment/view. I searched around in the forum and came to know few things
- One of the ways to handle this is by setting tag in instantiateItem() call back of the adapter and retreive the view by findViewbyTag. I could not understand how to implement this and I am also not sure if that will work for FragmentStatePagerAdapter.
- I explored other options as suggested in various forums, but cannot make them work.
My code is very much same as in a android http://developer.android.com/training/animation/screen-slide.html. Basic components are the same (Fragment activity xml with some display components including a textview, viewpager xml with just a view pager in it, a Fragment class and the main FragmentActivity class). in my FragmentActivity class I have added a pageChangelistener to my viewpager so as I can do my textview changes during onPageSelected().
Any help is is appreciated.
Adding the code for reference.
Public class myActivity extends FragmentActivity
//Variable declarations
protected void onCreate(Bundle savedInstanceState) {
ViewPager mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
View CurrView;
OnPageChangeListener pageChangelistener = new OnPageChangeListener() {
#Override
public void onPageSelected(int pageSelected) {
doTextViewChnges();//access the text view and update it based on pageSelected
---THIS IS WHERE I AM STUCK IN TRYING TO GET THE TEXTVIEW IN MY CURRENT FRAGMWNT/VIEW-------
}
mPager.setOnPageChangeListener(pageChangelistener);
}
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ScreenSlidePagerAdapter(android.support.v4.app.FragmentManager fm) {
super(fm);
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
return ScreenSlidePageFragment.create(position, <other parameters I want to pass>);
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
in your OnPageChangeListener:
OnPageChangeListener pageChangelistener = new OnPageChangeListener() {
#Override
public void onPageSelected(int pageSelected) {
ScreenSlidePageFragment currentFragment = (ScreenSlidePageFragment) mPagerAdapter.getItem(pageSelected)
doTextViewChnges();//access the text view and update it based on pageSelected
---THIS IS WHERE I AM STUCK IN TRYING TO GET THE TEXTVIEW IN MY CURRENT FRAGMWNT/VIEW-------
}
i assume you want to access your fragments from your activity and update the views.Fragments are parts of activities and their views can be accessed by them. Fragments are created dynamically in viewadapters and there is no straight forward and easy way to Tag them. You may simply access a fragment by its index number.first one is 0 second one is 1 and so on...
//access your fragment(in your case your first fragment)
//this part should be inside your doTextViewChnges()
Fragment fragment = (Fragment ) adapterViewPager
.getFragment(0);
if (fragment != null) {
//call a public method inside your fragment to update your text view
fragment .updateYourTextView();
}
Edit: inside your fragment create the following method and update your textview from there.
void updateYourTextView() {
yourTextView.setText("yourtext");
}

Swapping fragments in a viewpager

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)

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