Destroy item from the ViewPager's adapter after screen orientation changed
Asked Answered
S

1

25

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:

  1. 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.

  2. 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.

  3. I also tried to (re)attach the progressFragment to the fragment manager after the screen orientation changes, this also didn't work.

  4. I also tried to remove and then add again the progress fragment to the fragment manager after the activity was recreated, didn't work.

  5. 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!

Soyuz answered 12/3, 2013 at 9:33 Comment(6)
Maybe a stupid question, but do you actually re-add your fragments to the viewPager, after rotating your screen? Your activity gets destroyed on orientation change.Lifework
ah, good one, yes I'm adding them but forgot to add the code for it :). Thanks for pointing this out, I'll edit a bit my post. Cheers!Soyuz
Try actually adding your tabs again with .addTab() on onCreate. Is your problem only the progressFragment, or any?Lifework
the problem is just with this progressFragment, I'm having no other problems with the other pages which are PagerFragment instances. However what you suggested is not really possible since I'm not using a TabsAdapter, therefore no addTab() method, but thanks for the suggestion.Soyuz
ah sorry about that, I'll just update my post, so the setLoadingTrue() is done in the onPageSelected() callback if new data is required i.e if the view pager reached the end but there is still data that can be fetched from the api. Thanks for your suggestion, it was not the case, I found out the answer, cheers!Soyuz
Please add answer and mark as solved.Td
S
4

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!

Soyuz answered 27/3, 2013 at 19:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.