Reputation: 273
Trying to dynamically add Fragments to different pages in an Android ViewPager.
Dynamically adding a Fragment to the end of the list of Fragments works fine, but attempting to add to the next slot (the unseen page after the current viewable page). I've tried adding to the arraylist of fragments and using notifyDataSetChanged and trying to use set on each of the fragments after to set each to the previous item.
Edit: updated code
class MainPagerAdapter extends FragmentPagerAdapter {
public MainPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public PageFragment getItem(int position) {
return PageFragment.newInstance(Datamart.getInstance().getURLs().get(position));
}
@Override
public int getCount() {
return Datamart.getInstance().getURLs().size();
}
@Override
public int getItemPosition(Object object) {
PageFragment pageFragment = (PageFragment) object;
for ( int i = 0; i < getCount(); i++ ) {
if ( pageFragment.getURL().equals( Datamart.getInstance().getURLs().get(i) ) ) {
return i;
}
}
return POSITION_NONE;
}
}
Within the Datamart:
public class Datamart {
static Datamart instance;
private ArrayList<String> URLs;
private MainPagerAdapter mainPagerAdapter;
private ViewPager viewPager;
public Datamart() {
URLs = new ArrayList<>();
}
public static Datamart getInstance() {
if (instance == null) {
instance = new Datamart();
}
return instance;
}
public MainPagerAdapter getMainPagerAdapter() {
return mainPagerAdapter;
}
public void setMainPagerAdapter(MainPagerAdapter mainPagerAdapter) {
this.mainPagerAdapter = mainPagerAdapter;
}
public ViewPager getViewPager() {
return viewPager;
}
public void setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
}
public void addToEnd( String URL ) {
URLs.add(URL);
mainPagerAdapter.notifyDataSetChanged();
}
public void addNext( String URL ) {
URLs.add(viewPager.getCurrentItem() + 1, URL);
mainPagerAdapter.notifyDataSetChanged();
}
public ArrayList<String> getURLs() {
return URLs;
}
public void setURLs(ArrayList<String> URLs) {
this.URLs = URLs;
}
}
Upvotes: 2
Views: 906
Reputation: 30985
ViewPager
is doing its darndest to try and keep track of where all the fragments go, and you got it confused because you inserted a fragment in the middle without telling it.
Let's do an example. Say that you have two pages so that you are displaying the first page and the second page is the offscreen page to the right. You have fragment0
for the first page and fragment1
for the second page.
First, keep in mind that since ViewPager
is managing the offscreen pages to the immediate left and right, it already thinks it knows what the second page fragment is, which is fragment1
.
When you insert a fragment between those existing page fragments, ViewPager
is not going to ask you right away which fragment is at the second position, it's going to ask you if the fragment at the second position has changed.
This is where getItemPosition()
comes in. You didn't implement getItemPosition()
in your FragmentPagerAdapter
, so the default behavior of getItemPosition()
is to return POSITION_UNCHANGED
. (As you'll see, this is why adding a fragment at the end doesn't cause an error.)
So when you called notifyDataSetChanged()
, the ViewPager
called getCount()
and saw that instead of two pages, you now have three. It then called getItemPosition()
for page 0 and page 1 (the pages it knew about) and your default implementation told it that nothing had changed.
But something did change, you inserted a page/fragment in the middle of the two existing pages. ViewPager
asked you if the second page changed, and your getItemPosition()
should have returned 2
instead of POSITION_UNCHANGED
.
Now because your getCount()
returned 3
instead of 2
, ViewPager
knows that there is a new page at the end that it doesn't know about, so it calls getItem()
to get the new fragment.
Here is where the error occurs. Your getItem()
method returned fragment1
for page 3, and ViewPager
choked because you handed it the fragment that it already knew about. Specifically, it uses the fragment's tag for some housekeeping. It creates a tag out the the page's container id and the item position (page number). So at any given time it expects that either (a) the tag is null, or (b) the tag will be the same value that it already calculated from the container id and the item position. Neither of those conditions were true, so that's why IllegalStateException
was thrown.
So how do you fix this?
You implement a getItemPosition()
that does the right thing. When your getItemPosition()
is called, you scan through your list of fragments, and when you find a matching fragment, you return the index of the fragment. If you removed a fragment such that there's no matching fragment in your list, you return POSITION_NONE
to tell the ViewPager
that the page should be removed. ViewPager
will then call your getItem()
method to get the new page. In this way, ViewPager
can always stay in sync with your adapter.
When I write a FragmentPagerAdapter
, I don't even use the fragments in the model. If I were writing your adapter, I would just have a list of URLs, since it looks like each page/fragment has it's own unique URL.
I would also implement a getUrl()
method on the fragment class to get the URL that the fragment was created with.
So for getItemPosition()
, I would call getUrl()
on the fragment passed in, and search my adapter's list for that URL. Then I would return the index of the URL in the list, or POSITION_NONE
if it's not there any more.
Then in getItem()
, I would instantiate a new fragment using the URL at the list position that was passed in. My rule is that I never instantiate a fragment until the ViewPager
specifically requests it by calling getItem()
.
This way, only the ViewPager
is accessing the fragments, and I don't run into these types of issues.
Also, don't make any changes to the adapter model between the time when ViewPager
calls startUpdate()
to when it calls finishUpdate()
. This is how ViewPager
tells your adapter that it's in a "critical section" of figuring out where all the page fragments go.
PagerAdapter
s can be a bear to work with. Put some debug logging in your getCount()
, getItemPosition()
, and getItem()
methods so that you can get a feel for how ViewPager
uses your adapter to manage its pages.
Upvotes: 2