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 theonTaskSuccess()
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!