Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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!

like image 383
Radu Comaneci Avatar asked Mar 12 '13 09:03

Radu Comaneci


People also ask

How to set ViewPager in android?

You can create swipe views using AndroidX's ViewPager widget. To use ViewPager and tabs, you need to add a dependency on ViewPager and on Material Components to your project. To insert child views that represent each page, you need to hook this layout to a PagerAdapter .

How to create ViewPager in android studio?

Android ViewPager widget is found in the support library and it allows the user to swipe left or right to see an entirely new screen. Today we're implementing a ViewPager by using Views and PagerAdapter. Though we can implement the same using Fragments too, but we'll discuss that in a later tutorial.


1 Answers

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!

like image 70
Radu Comaneci Avatar answered Sep 19 '22 13:09

Radu Comaneci