web-dev-qa-db-fra.com

Détruisez l'élément de l'adaptateur du ViewPager après que l'orientation de l'écran a été modifiée

J'ai donc un problème avec la destruction (suppression) d'une page du ViewPager après que l'orientation de l'écran a changé. Je vais essayer de décrire le problème dans les lignes suivantes.

J'utilise le FragmentStatePagerAdapter pour l'adaptateur du ViewPager et une petite interface qui décrit comment un pager de vue sans fin devrait fonctionner. L'idée derrière cela est que vous pouvez faire défiler vers la droite jusqu'à la fin du ViewPager. Si vous pouvez charger plus de résultats à partir d'un appel d'API, une page de progression s'affiche jusqu'à ce que les résultats arrivent.

Tout va bien jusqu'ici, maintenant le problème vient. Si pendant ce processus de chargement, je fais pivoter l'écran (cela n'affectera pas l'appel API qui est essentiellement un AsyncTask), lorsque l'appel revient, l'application se bloque en me donnant cette 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)

Après avoir creusé un peu dans le code de la bibliothèque, il semble que le champ de données mIndex du fragment soit inférieur à 0 Dans ce cas, et cela lève cette exception.

Voici le code de l'adaptateur de pager:

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;
        }
    }
}

La fonction de rappel onPageSelected() ci-dessous, qui démarre essentiellement l'appel api si nécessaire:

 @Override
    public void onPageSelected(int currentPosition) {
        updatePagerIndicator(currentPosition);
        activity.invalidateOptionsMenu();
        if (requestNextApiPage(currentPosition)) {
            pagerAdapter.setLoadingData(true);
            requestNextPageController.requestNextPageOfData(this);
        }

Maintenant, il convient également de dire ce que fait l'appel API après avoir fourni les résultats. Voici le rappel:

@Override
public boolean onTaskSuccess(Context arg0, List<DataItem> result) {
    data = result;
    pagerAdapter.setLoadingData(false);
    pagerAdapter.setData(result);
    activity.invalidateOptionsMenu();

    return true;
}

Ok, maintenant parce que la méthode setData() appelle la notifiyDataSetChanged(), cela appellera getItemPosition() pour les fragments qui sont actuellement dans le tableau currentFragments. Bien sûr, pour l'élément progress, il renvoie POSITION_NONE Car je veux supprimer cette page, donc cela appelle essentiellement le rappel destroyItem() du PagedSingleDataAdapter. Si je ne fais pas pivoter l'écran, tout fonctionne bien, mais comme je l'ai dit si je le fais pivoter lorsque l'élément progress est affiché et que l'appel API n'est pas encore terminé, le rappel destroyItem() sera invoqué après le redémarrage de l'activité.

Je devrais peut-être aussi dire que j'héberge le ViewPager dans un autre fragment et non dans une activité, donc le OutterFragment héberge le ViewPager. J'instancie le pagerAdapter dans la fonction de rappel onActivityCreated() du OutterFragment et j'utilise la setRetainInstance(true) de sorte que lorsque l'écran tourne le pagerAdapter reste le même (rien ne doit être changé, non?), codez ici:

if (pagerAdapter == null) {
    pagerAdapter = new PagedSingleDataAdapter(this, data);
}
pager.setAdapter(pagerAdapter);

if (savedInstanceState == null) {
    pager.setOnPageChangeListener(this);
    pager.setCurrentItem(currentPosition);
}

En résumé maintenant, le problème est:

Si j'essaie de supprimer l'élément progress du ViewPager après qu'il a été instancié et que l'activité a été détruite et recréée (l'orientation de l'écran a changé), j'obtiens l'exception ci-dessus (le pagerAdapter reste le même, donc tout à l'intérieur reste également le même, références etc… puisque le OutterFragment qui héberge le pagerAdapter n'est pas détruit est seulement détaché de l'activité puis ré-attaché). Il se passe probablement quelque chose avec le gestionnaire de fragments, mais je ne sais vraiment pas quoi.

Ce que j'ai déjà essayé:

  1. Essayer de supprimer mon fragment de progression en utilisant une autre technique, c'est-à-dire sur le rappel onTaskSuccess() J'essayais de supprimer le fragment du gestionnaire de fragments, n'a pas fonctionné.

  2. J'ai également essayé de masquer l'élément progress au lieu de le supprimer complètement du gestionnaire de fragments. Cela a fonctionné à 50%, car la vue n'était plus là, mais j'avais une page vide, donc ce n'est pas vraiment ce que je cherche.

  3. J'ai également essayé de (re) joindre le progressFragment au gestionnaire de fragments après les changements d'orientation de l'écran, cela n'a pas fonctionné non plus.

  4. J'ai également essayé de supprimer puis d'ajouter à nouveau le fragment de progression au gestionnaire de fragments après que l'activité a été recréée, n'a pas fonctionné.

  5. J'ai essayé d'appeler la fonction destroyItem() manuellement à partir de la fonction de rappel onTaskSuccess() (ce qui est vraiment, vraiment moche) mais n'a pas fonctionné.

Désolé les gars pour un si long post, mais j'essayais d'expliquer le problème du mieux que je pouvais pour que vous puissiez le comprendre.

Toute solution, recommandation est très appréciée.

Merci!

MISE À JOUR: SOLUTION TROUVÉE OK, cela a donc pris du temps. Le problème était que le rappel destroyItem () était appelé deux fois sur le fragment de progression, une fois lorsque l'orientation de l'écran changeait, puis à nouveau une fois l'appel api terminé. Voilà pourquoi l'exception. La solution que j'ai trouvée est la suivante: Gardez un suivi si l'appel api s'est terminé ou non et détruisez le fragment de progression juste dans ce cas, code ci-dessous.

@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);
            }
        }

puis cet apiCallFinished est défini sur false dans le constructeur de l'adaptateur et sur true dans le rappel onTaskSuccess(). Et ça marche vraiment!

24
Radu Comaneci

MISE À JOUR: SOLUTION TROUVÉE OK, cela a donc pris du temps. Le problème était que le rappel destroyItem () était appelé deux fois sur le fragment de progression, une fois lorsque l'orientation de l'écran changeait, puis une fois après la fin de l'appel api. Voilà pourquoi l'exception. La solution que j'ai trouvée est la suivante: Gardez un suivi si l'appel API s'est terminé ou non et détruisez le fragment de progression juste dans ce cas, code ci-dessous.

@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);
            }
        }

puis cet apiCallFinished est défini sur false dans le constructeur de l'adaptateur et sur true dans le rappel onTaskSuccess (). Et ça marche vraiment!

4
Radu Comaneci