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.
Related
When I start the app everything works ok but when I rotate to landscape it crashes because in the Fragment there is a field that is NULL.
I dont use setRetainInstance(true) or adding Fragments to FragmentManagerI create new Fragments on app start and when app rotate.
In the Activity OnCreate() I create the Fragment and adding them to the viewPager like this.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ParentBasicInfoFragment parentBasicInfoFragment = new ParentBasicInfoFragment();
ParentUTCFragment parentUTCFragment = new ParentUTCFragment();
ParentEventsFragment parentEventsFragment = new ParentEventsFragment();
this.mFragments = new ArrayList<>();
this.mFragments.add(parentBasicInfoFragment);
this.mFragments.add(parentUTCFragment);
this.mFragments.add(parentEventsFragment);
this.viewpage.setOffscreenPageLimit(3);
setCurrentTab(0);
this.viewpage.setAdapter(new MainActivityPagerAdapter(getSupportFragmentManager(), this.mFragments));
}
Then I have a test button on the app that when I press it will do like
public void test(View view) {
((BaseFragment) MainActivity.this.mFragments.get(MainActivity.this.viewpage.
getCurrentItem())).activityNotifiDataChange("hello");
}
This will work and the current Fragments in the ViewPager have the method, activityNotifiDataChange() that are being called and all is ok.
When I rotate the app and do the same thing pressing the button the activityNotifiDataChange() is being called alright but there a null pointer exception because the ArrayList<Fragment> mFragment is now NULL.
HereĀ“s a small sample Android Studio project showing this behavior:
https://drive.google.com/file/d/1Swqu59HZNYFT5hMTqv3eNiT9NmakhNEb/view?usp=sharing
Start app and press button named "PRESS TEST", then rotate device and press the button again and watch the app crash
UPDATE SOLUTION thanks #GregMoens and #EpicPandaForce
public class MainActivityPagerAdapter extends PersistenPagerAdapter<BaseFragment> {
private static int NUM_ITEMS = 3;
public MainActivityPagerAdapter(FragmentManager fm) {
super(fm);
}
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ParentBasicInfoFragment.newInstance(0, "Page # 1");
case 1:
return ParentUTCFragment.newInstance(1, "Page # 2");
case 2:
return ParentEventsFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
}
public abstract class PersistenPagerAdapter<T extends BaseFragment> extends FragmentPagerAdapter {
private SparseArray<T> registeredFragments = new SparseArray<T>();
public PersistenPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public T instantiateItem(ViewGroup container, int position) {
T fragment = (T)super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public T getRegisteredFragment(ViewGroup container, int position) {
T existingInstance = registeredFragments.get(position);
if (existingInstance != null) {
return existingInstance;
} else {
return instantiateItem(container, position);
}
}
}
The main problem I see with your app is your misunderstanding with how FragmentPagerAdapter works. I see this a lot and it's due to lack of good javadocs on the class. The adapter should be implemented so that getItem(position) returns a new fragment instance when called. And then getItem(position) will only be called by the pager when it needs a new instance for that position. You should not pre-create the fragments and pass then into the adapter. You should also not be holding strong references to the fragments from either your activity or from parent fragments (like ParentBasicInfoFragment). Because remember, the fragment manager is managing fragments and you are also managing fragments by newing them and keeping references to them. This is causing a conflict and after rotation, you are trying to invoke activityNotifiDataChange() on a fragment that is not actually initialized (onCreate() was not called). Using the debugger and tracking object IDs will confirm this.
If you change your code so that the FragmentPagerAdapter creates the fragments when they are needed and don't store references to fragments or lists of fragments, you will see much better results.
this.mFragments = new ArrayList<>();
Because this is wrong. You should never hold a reference to the fragment list if you are using ViewPager. Just return new instance of Fragment from getItem(int position) of FragmentPagerAdapter and you are good to go.
But to fix your code, you must delete mFragments entirely.
See https://stackoverflow.com/a/58605339/2413303 for more details.
Use setRetainInstance(true) is not a good approach. If you need to same some simple information such as: position of recyclerView, selected item of recyclerView, maybe some model(Parcelable) you could do it with method onSaveInstanceState / onRestoreInstanceState, there is one limitation is 1MB. Here is an example with animations how it works.
For more durable persistance use SharedPreferences, Room(Google ORM) or you could try to use ViewModel with LiveData (best approach some data which should live while user session).
//change in AndroidManifest.xml file,
<activity
android:name=".activity.YourActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="sensor"
/>
//YourActivity in which you defines fragment.
//may be it helps
I have built an application with a navbar on the left, whose main content is a ViewPager
The ViewPager slides between two different views.
When the user selects something from the navgation bar, I send a message to the ViewPager's adapter (I have tried both FragmentPagerAdapter and FragmentStatePagerAdapter for this, both won't work) which sets an internal variable and calls notifyDatasetChanged();
The problem is that the getCount() method always returns 2 , so when the adapter checks to see if the dataset has changed, it sees that the items are still 2 and does not go on to call getItem(position).
The getItem(position) returns different fragments according to the value of the internal variable that is set before notifyDatasetChanged();
I tried overriding getItemId(position) in case the pager checks for the id, but it seems to not bother after checking the count.
Is there a way to force the adapter to rebuild the fragments when notifyDatasetChanged() is called?
Thanks in advance for any help you can provide
Edit: here is the code I am currently using:
public class ContentAdapter extends FragmentPagerAdapter {
private ViewedSection _section = ViewedSection.Main;
public ContentAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
ViewedScreen screen = get_screen(position);
return screen == null ? null : FragmentFactory.GetFragment(get_screen(position));
}
#Override
public int getCount() {
return 2;
}
private ViewedScreen get_screen(int position) {
//code to resolve which screen will be shown according to the current position and _section
}
public void set_page(ViewedSection section) {
this._section = section;
notifyDataSetChanged();
}
}
So when the user clicks on a NavBar item, I call ((ContentAdapter)_pager.getAdapter()).set_page(section);
For this, you need to override
public int getItemPosition (Object object)
Return POSITION_UNCHANGED if you don't want to replace the fragment. Return POSITION_NONE if you want to replace the fragment. (Also, you can return a new position to move the fragment to.)
A common override is
public int getItemPosition (Object object) {
return POSITION_NONE;
}
which will just rebuild everything.
Trying to programmatically add a fragment page to my ViewPager, I get:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged!
Expected adapter item count: 3, found: 2
Pager id: com.my.app:id/view_pager
Pager class: class android.support.v4.view.ViewPager
Problematic adapter: class com.my.app.ui.BaseFragmentPagerAdapter
at android.support.v4.view.ViewPager.populate(ViewPager.java:1000)
at android.support.v4.view.ViewPager.populate(ViewPager.java:952)
at ...
I'm simply calling these few lines on my FragmentPagerAdapter implementation:
adapter.addFragment(new Fragment(), "FIRST");
adapter.addFragment(new Fragment(), "SECOND");
pager.setAdapter(adapter);
//later... (on click of a button)
adapter.addFragment(new Fragment(), "THIRD");
adapter.notifyDataSetChanged();
It actually adds the third page, but when I try to swipe there, it fails with the above mentioned exception. Until today I thought I had a pretty complete understanding of how adapters work. Now I can't figure out what's wrong.
From debugging, it seems that all the time adapter.getCount() correctly returns 3 (after adding the third page), but when I'm there to the third page it eventually returns 2 and breaks, as if someone called destroyItem() on it, but that's not me.
Here's my simple class:
public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
private SparseArray<Fragment> mFragments;
private ArrayList<String> mFragmentTitles;
public BaseFragmentPagerAdapter(FragmentManager manager) {
super(manager);
this.mFragments = new SparseArray<>();
this.mFragmentTitles = new ArrayList<>();
}
public void addFragment(Fragment f, String title) {
this.mFragments.append(mFragments.size() , f);
this.mFragmentTitles.add(title);
}
#Override
public Fragment getItem(int position) {
return this.mFragments == null ? null : this.mFragments.get(position) ;
}
#Override
public int getItemPosition(Object object) {
return this.mFragments.indexOfValue((Fragment) object);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
this.mFragments.remove(position);
this.mFragmentTitles.remove(position);
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitles.get(position);
}
#Override
public int getCount() {
return mFragmentTitles.size();
}
}
Note that nothing changes if I use a FragmentStatePagerAdapter rather than FragmentPagerAdapter.
I will answer this myself since I found the answer while writing the question (as often). I'm not sure this is the best solution, but it worked.
Basically, when going to page 3, since it's not directly swipable-to, the adapter will call destroyItem() on page 1. Shouldn't FragmentPagerAdapter hold all items in memory without destroying them?
Well, I do hold fragments in memory through the mFragments fields. The call to destroyItem() destroys the associated view of the fragment, but should not destroy the fragment itself (I might be slightly wrong here, but you get the point).
So it's up to you (me) to keep the fragments in memory, and not invalidating them on destroyItem(). Specifically, I had to remove these two lines:
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
//removed: this.mFragments.remove(position);
//removed: this.mFragmentTitles.remove(position);
}
This way getCount() keeps returning correctly 3, and when you are back to page 1, the adapter can get its fragment through getItem().
Edit
After dealing with it for a day, I can say that, at a first glance, having a FragmentPagerAdapter that does not hold fragments in memory makes no sense to me.
It is documented that it should be used just for a few static fragments. ViewPager, by default, holds the current item, the one before and the one after, but you can tune this setting through viewPager.setOffscreenPageLimit().
If you destroy them during destroyItem(), things get bad. It will be easy to reinstantiate them through some logic in getItem(), but it is quite hard to save their instance state. For instance, onSaveInstanceState(Bundle outstate) is not called after destroyItem().
If your fragments are many and you want to accept the possibility that one of more get destroyed, you just switch to FragmentStatePagerAdapter, but that's another story: it automatically calls onSaveInstanceState and lets you retain what you need to retain.
Using FragmentPagerAdapter, thus renouncing on the state-saving features of FragmentStatePagerAdapter, makes no sense if you don't retain. I mean, either you retain the instances, or you save their state (as suggested in the comments). For the latter, though, I would go for FragmentStatePagerAdapter that makes it easy.
(Note: I'm not talking about retaining instances when the activity gets destroyed, but rather when a page of the ViewPager goes through destroyItem and the associated fragment goes through onDestroyView()).
I have:
A ViewPager with a FragmentPagerAdapter on (or a FragmentStatePagerAdapter, doesn't really solve my problem).
A fixed number of fragments. They all share the same layout, but have TextViews that need to be set differently;
An AsyncTask that queries my database and retrieves content to be set into the TextViews.
So my code was:
public class StatsPagerAdapter extends FragmentPagerAdapter {
static final int FRAGMENT_COUNT = 5;
private Parameters[] sectionData;
public StatsPagerAdapter(FragmentManager manager) {
super(manager);
this.sectionData = new Parameters[FRAGMENT_COUNT];
}
#Override
public Fragment getItem(int position) {
return StatsSectionFragment.getInstance(this.sectionData[position]);
}
#Override
public int getCount() {
return FRAGMENT_COUNT;
}
public void setSectionData(int position, Parameters sectionData) {
this.sectionData[position] = sectionData;
notifyDataSetChanged();
}
}
So I'm passing sectionData[position] to the getInstance() method of my generic sub-fragment. That data should differentiate each instance of the fragments loaded into the ViewPager.
At first I'll be passing an empty reference, but then in my async class:
#Override
protected void onProgressUpdate(Parameters... sectionValues) {
super.onProgressUpdate(sectionValues);
mPagerAdapter.setSectionData(sectionId, sectionValues[0]);
}
That should call the setSectionData() above, update sectionData[position] and generate a call to notifiyDataSetChanged(). I was hoping that doing so would make the adapter retrieve all its items again, thus calling getItem() again, and loading new fragments.
Sadly it does not. So right now:
if fragments (i.e., getItem()) are created before my async task result are published, that item will stay empty (i.e.,visible fragment, but with empty text views since I never called getInstance(non-null stuff).
That happens for fragment 0 && fragment 1.
if fragments are created after my async task has ended (fragment 2 to end, because getItem() is called only when you reach that fragment by swiping), then the first and only call to getItem() produces a getInstance(non-null stuff), and that's ok.
How can I reload content into those already-there fragments, or force the adapter to call getItem() and update its views?
Add this to your adapter:
#Override
public int getItemPosition(Object object){
return PagerAdapter.POSITION_NONE;
}
This make your adapter call getItem again when you call notifyDataSetChanged();
I understand that the offscreen page limit for a viewpager must be at least 1. I am trying to dynamically change the fragments in my viewpagers as I am constantly grabbing information from a server.
For example, I have Fragments A, B, C instantiated when I am viewing Fragment B.
I want to change Fragment A's info, so I update it and call notifyDataSetChanged(). I have not created a new Fragment and inserted it in its place, but changed the imageview associated with the original fragment.
However, once I try to navigate to A, I run into an error saying "Fragment is not currently in the FragmentManager "
Can anyone explain to me how I'd be able to jump back and forth between immediate pages in a viewpager while allowing these pages to change their views?
I didn't do that, but my suggestion will be not to try to. Android does a lot of magic under the hood, and it is very possible you'll face a lot of issues trying to implement what you want. I had an experience with ListView when I was trying to save contentView for each list item, so that Android will render them only once, after the whole day I gave up the idea because every time I've changed the default behavior, something new came up (like exceptions that views are having two parents). I've managed to implement that, but the code was really awful.
Why don't you try, for example, save the image you've downloaded on the disc, and retrieve if when fragment actually appears on the screen ? Picasso library could help in this case
To do that, you should create a FragmentPagerAdapter that saves references to your pages as they are created. Something like this:
public class MyPagerAdapter extends FragmentPagerAdapter {
SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch (position)
{
case 0:
return MyListFragment.newInstance(TAB_NAME_A);
case 1:
return MyListFragment.newInstance(TAB_NAME_B);
case 2:
return MyListFragment.newInstance(TAB_NAME_C);
default:
return null;
}
}
#Override
public int getCount() {
// Show 3 total pages.
return TOTAL_PAGES;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}
You can access a particular page like this:
((MyListFragment) myPagerAdapter.getRegisteredFragment(0)).updateUI();
Where updateUI() is a custom method that updates your list on that page and calls notifyDataSetChanged().