web-dev-qa-db-fra.com

Fragment dans ViewPager non restauré après popBackStack

Problème

Un fragment n'est pas rattaché à son ViewPager d'hébergement après son retour d'un autre fragment.

Situation

Une activité hébergeant un fragment dont la disposition contient un ViewPager (PageListFragment dans l'exemple ci-dessous). Le ViewPager est rempli par un FragmentStateViewPagerAdapter. Les fragments uniques hébergés à l'intérieur du pager (PageFragment dans l'exemple ci-dessous) peuvent ouvrir des listes de sous-pages, contenant un nouvel ensemble de pages.

Comportement

Tout fonctionne bien tant que le bouton de retour n'est pas enfoncé. Dès que l'utilisateur ferme l'une des sous-listes de pages, la liste précédente est recréée, mais sans la page qui était affichée précédemment. Glisser à travers les autres pages de la PageList parent fonctionne toujours.

Code

Un exemple d'application peut être trouvé sur github :

Activité

public class MainActivity extends FragmentActivity {

private static final String CURRENT_FRAGMENT = MainActivity.class.getCanonicalName() + ".CURRENT_FRAGMENT";

public static final String ARG_PARENTS = "Parents";

public void goInto(String mHostingLevel, String mPosition) {
    Fragment hostingFragment = newHostingFragment(mHostingLevel, mPosition);
    addFragment(hostingFragment);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    addBaseFragment();
}

private void addBaseFragment() {
    Fragment hostingFragment = newHostingFragment("", "");
    addFragment(hostingFragment);
}

private Fragment newHostingFragment(String mHostingLevel, String oldPosition) {
    Fragment hostingFragment = new PageListFragment();
    Bundle args = new Bundle();
    args.putString(ARG_PARENTS, mHostingLevel + oldPosition +" > ");
    hostingFragment.setArguments(args);
    return hostingFragment;
}

private void addFragment(Fragment hostingFragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.fragmentSpace, hostingFragment, CURRENT_FRAGMENT);
    transaction.addToBackStack(null);
    transaction.commit();
}

}

PageListFragment

public class PageListFragment extends Fragment {

private String mParentString;

public PageListFragment() {
    // Required empty public constructor
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_hosting, container, false);
}

@Override
public void onResume() {
    mParentString = getArguments().getString(MainActivity.ARG_PARENTS);
    ViewPager viewPager = (ViewPager) getView().findViewById(R.id.viewPager);
    viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));
    super.onResume();
}

private static class SimpleFragmentStatePagerAdapter extends FragmentStatePagerAdapter {

    private String mHostingLevel;

    public SimpleFragmentStatePagerAdapter(FragmentManager fm, String hostingLevel) {
        super(fm);
        this.mHostingLevel = hostingLevel;
    }

    @Override
    public Android.support.v4.app.Fragment getItem(int position) {
        PageFragment pageFragment = new PageFragment();
        Bundle args = new Bundle();
        args.putString(MainActivity.ARG_PARENTS, mHostingLevel);
        args.putInt(PageFragment.ARG_POSITION, position);
        pageFragment.setArguments(args);
        return pageFragment;
    }

    @Override
    public int getCount() {
        return 5;
    }
}
}

PageFragment

public class PageFragment extends Fragment {

public static final String ARG_POSITION = "Position";

private String mHostingLevel;
private int mPosition;

public PageFragment() {
    // Required empty public constructor
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View contentView = inflater.inflate(R.layout.fragment_page, container, false);
    setupTextView(contentView);
    setupButton(contentView);
    return contentView;
}

private void setupTextView(View contentView) {
    mPosition = getArguments().getInt(ARG_POSITION);
    mHostingLevel = getArguments().getString(MainActivity.ARG_PARENTS);
    TextView text = (TextView) contentView.findViewById(R.id.textView);
    text.setText("Parent Fragments " + mHostingLevel + " \n\nCurrent Fragment "+ mPosition);
}

private void setupButton(View contentView) {
    Button button = (Button) contentView.findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            openNewLevel();
        }
    });
}

protected void openNewLevel() {
    MainActivity activity = (MainActivity) getActivity();
    activity.goInto(mHostingLevel, Integer.toString(mPosition));
}

}
54
Paul

Après une longue enquête, cela s'avère être un problème avec le gestionnaire de fragments.

Lorsque vous utilisez une construction comme celle ci-dessus, la transaction de fragment pour rattacher le fragment à la liste de pages est ignorée en silence. C’est essentiellement le même problème qui cause

Java.lang.IllegalStateException: Recursive entry to executePendingTransactions 

lorsque vous essayez de modifier les fragments à l'intérieur du FragmentPager.

La même solution, comme pour les problèmes avec cette erreur, est également applicable ici. Lors de la construction de FragmentStatePagerAdapter, fournissez le gestionnaire de fragments enfant correct.

Au lieu de

    viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));

faire

    viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getChildFragmentManager(),mParentString));

Voir aussi: github

143
Paul

Ce que Paul a omis de mentionner, c'est que si vous utilisez getChildFragmentManager, vous rencontrerez le problème "écran vide à contre-pression".

10
Qylin

La hiérarchie dans mon cas était:

MainActivity-> MainFragment-> TabLayout + ViewPager-> AccountsFragment + SavingsFragment + InvestmentsFragment etc.

Le problème que j'avais était que je ne pouvais pas utiliser childFragmentManager pour la raison qu'un clic sur l'élément Account view (Qui réside dans l'un des Fragments du ViewPager) nécessaire pour remplacer MainFragment, c'est-à-dire tout l'écran.

enter image description here

L'utilisation de MainFragments Host Fragment c'est-à-dire le passage de getFragmentManager() a permis le remplacement, MAIS lors du pop-back, j'ai fini avec cet écran:

enter image description here

Cela était également apparent en regardant l'inspecteur de disposition où le ViewPager est vide.

enter image description here

Apparemment, en regardant les Fragment restaurés, vous remarquerez que leur View est restauré mais ne correspondra pas à la hiérarchie de l'état sauté. Afin d'avoir un impact minimum et de ne pas forcer une recréation des Fragments j'ai réécrit FragmentStatePagerAdapter avec les changements suivants:

J'ai copié le code entier de FragmentStatePagerAdapter et changé

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
...
}

avec

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }

            mCurTransaction.detach(f);
            mCurTransaction.attach(f);

            return f;
        }
    }
...
}

De cette façon, je m'assure effectivement que les Fragment restaurés sont ré-attachés au ViewPager.

Supprimez tous les fragments de page, ce qui leur permettra d'être rajoutés plus tard

Les fragments de page ne sont pas attachés lorsque vous revenez à l'écran du visualiseur car FragmentStatePagerAdapter ne les reconnecte pas. Pour contourner le problème, supprimez tous les fragments dans le viewpager après l'appel de popbackstack (), ce qui leur permettra d'être rajoutés par votre code initial.

[Cet exemple est écrit en Kotlin]

//Clear all fragments from the adapter before they are re-added.
for (i: Int in 0 until adapter.count) {
    val item = childFragmentManager.findFragmentByTag("f$i")
    if (item != null) {
        adapter.destroyItem(container!!, i, item)
    }
}
0
James Porter