web-dev-qa-db-fra.com

Problèmes avec la pile de retour Android Fragment

J'ai un gros problème avec la façon dont fonctionne le backstack de fragments Android et je serais très reconnaissant pour toute aide offerte.

Imaginez que vous avez 3 fragments

[1] [2] [3]

Je veux que l'utilisateur puisse naviguer [1] > [2] > [3] mais au retour (en appuyant sur le bouton retour) [3] > [1].

Comme je l'aurais imaginé, cela serait accompli en n'appelant pas addToBackStack(..) lors de la création de la transaction qui introduit fragment [2] dans le détenteur de fragment défini en XML.

La réalité semble être que si je ne veux pas que [2] réapparaisse lorsque l'utilisateur appuie sur le bouton précédent sur [3], je ne dois pas appeler addToBackStack dans la transaction qui affiche le fragment [3]. Cela semble complètement contre-intuitif (venant peut-être du monde iOS).

Quoi qu'il en soit, si je le fais de cette façon, lorsque je passe de [1] > [2] et que j'appuie en arrière, je reviens à [1] comme prévu.

Si je vais [1] > [2] > [3] et que j'appuie de nouveau sur je retourne à [1] (comme prévu) . Maintenant, le comportement étrange se produit lorsque je tente de sauter à [2] à partir de [1]. Tout d'abord, [3] est brièvement affiché avant que [2] ne soit visible. Si j'appuie en arrière à ce moment-là, [3] s'affiche et si j'appuie de nouveau en arrière, l'application se ferme.

Quelqu'un peut-il m'aider à comprendre ce qui se passe ici? 


Et voici le fichier XML de mise en page de mon activité principale:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
          Android:layout_width="fill_parent"
          Android:layout_height="fill_parent"
          Android:orientation="vertical" >

<fragment
        Android:id="@+id/headerFragment"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        class="com.fragment_test.FragmentControls" >
    <!-- Preview: layout=@layout/details -->
</fragment>
<FrameLayout
        Android:id="@+id/detailFragment"
        Android:layout_width="match_parent"
        Android:layout_height="fill_parent"

        />



Update C'est le code que j'utilise pour construire par la hiérarchie de navigation

    Fragment frag;
    FragmentTransaction transaction;


    //Create The first fragment [1], add it to the view, BUT Dont add the transaction to the backstack
    frag = new Fragment1();

    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.commit();

    //Create the second [2] fragment, add it to the view and add the transaction that replaces the first fragment to the backstack
    frag = new Fragment2();

    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.addToBackStack(null);
    transaction.commit();


    //Create third fragment, Dont add this transaction to the backstack, because we dont want to go back to [2] 
    frag = new Fragment3();
    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.commit();


     //END OF SETUP CODE-------------------------
    //NOW:
    //Press back once and then issue the following code:
    frag = new Fragment2();
    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.addToBackStack(null);
    transaction.commit();

    //Now press back again and you end up at fragment [3] not [1]

Merci beaucoup

112
Chris Birch

Explication: sur ce qui se passe ici?

Si nous gardons à l'esprit que .replace() est égal à .remove().add(), nous le savons par la documentation:

Remplacer un fragment existant qui a été ajouté à un conteneur. Ceci est essentiellement identique à l'appel de remove(Fragment) pour tous les fragments actuellement ajoutés qui ont été ajoutés avec les mêmes containerViewId et ensuite add(int, Fragment, String) avec les mêmes arguments donnés ici.

alors ce qui se passe est comme ça (j'ajoute des chiffres au fragment pour le rendre plus clair):

// transaction.replace(R.id.detailFragment, frag1);
Transaction.remove(null).add(frag1)  // frag1 on view

// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag1).add(frag2).addToBackStack(null)  // frag2 on view

// transaction.replace(R.id.detailFragment, frag3);
Transaction.remove(frag2).add(frag3)  // frag3 on view

(ici tout ce qui est trompeur commence à arriver)

Rappelez-vous que .addToBackStack() enregistre uniquement transaction et non le fragment en tant que tel! Alors maintenant nous avons frag3 sur la mise en page:

< press back button >
// System pops the back stack and find the following saved back entry to be reversed:
// [Transaction.remove(frag1).add(frag2)]
// so the system makes that transaction backward!!!
// tries to remove frag2 (is not there, so it ignores) and re-add(frag1)
// make notice that system doesn't realise that there's a frag3 and does nothing with it
// so it still there attached to view
Transaction.remove(null).add(frag1) //frag1, frag3 on view (OVERLAPPING)

// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag3).add(frag2).addToBackStack(null)  //frag2 on view

< press back button >
// system makes saved transaction backward
Transaction.remove(frag2).add(frag3) //frag3 on view

< press back button >
// no more entries in BackStack
< app exits >

Solution possible

Pensez à implémenter FragmentManager.BackStackChangedListener pour surveiller les modifications dans la pile arrière et appliquer votre logique dans onBackStackChanged() methode:

193
Arvis

Droite!!! Après avoir tiré beaucoup de poils, j'ai enfin trouvé comment le faire fonctionner correctement.

Il semble que fragment [3] ne soit pas supprimé de la vue lorsque vous appuyez sur la touche Retour, vous devez donc le faire manuellement!

Tout d’abord, n’utilisez pas replace (), mais utilisez remove et add séparément. Il semble que replace () ne fonctionne pas correctement.

La partie suivante consiste à remplacer la méthode onKeyDown et à supprimer le fragment en cours chaque fois que le bouton Précédent est enfoncé.

@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
    if (keyCode == KeyEvent.KEYCODE_BACK)
    {
        if (getSupportFragmentManager().getBackStackEntryCount() == 0)
        {
            this.finish();
            return false;
        }
        else
        {
            getSupportFragmentManager().popBackStack();
            removeCurrentFragment();

            return false;
        }



    }

    return super.onKeyDown(keyCode, event);
}


public void removeCurrentFragment()
{
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

    Fragment currentFrag =  getSupportFragmentManager().findFragmentById(R.id.detailFragment);


    String fragName = "NONE";

    if (currentFrag!=null)
        fragName = currentFrag.getClass().getSimpleName();


    if (currentFrag != null)
        transaction.remove(currentFrag);

    transaction.commit();

}

J'espère que cela t'aides!

33
Chris Birch

Tout d’abord, merci à @Arvis pour une explication qui ouvre les yeux.

Je préfère solution différente à la réponse acceptée ici pour ce problème. Je n'aime pas jouer avec le comportement de retour en arrière plus qu'absolument nécessaire et quand j'ai essayé d'ajouter et de supprimer des fragments sans mon retour de pile par défaut lorsque le bouton Précédent est enfoncé, je me suis retrouvé dans l'enfer des fragments :) ajoutez f2 sur f1 lorsque vous le supprimez, f1 n'appelle aucune méthode de rappel telle que onResume, onStart, etc., ce qui peut être très malheureux.

Quoi qu'il en soit, voici comment je le fais:

Présentement affiché n'est que le fragment f1.

f1 -> f2

Fragment2 f2 = new Fragment2();
this.getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content,f2).addToBackStack(null).commit();

rien d'extraordinaire ici. Que dans fragment F2, ce code vous amène à fragmenter F3.

f2 -> f3

Fragment3 f3 = new Fragment3();
getActivity().getSupportFragmentManager().popBackStack();
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();

En lisant la documentation, je ne suis pas sûr que cela fonctionne, il est dit que cette méthode de transaction contextuelle est asynchrone, et un meilleur moyen serait d'appeler popBackStackImmediate (). Mais autant que je sache sur mes appareils, cela fonctionne parfaitement.

Cette alternative serait:

final FragmentActivity activity = getActivity();
activity.getSupportFragmentManager().popBackStackImmediate();
activity.getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();

En fait, ici, il sera bref de revenir à F1 avant de passer à F3, il ya donc un léger problème.

C’est tout ce que vous avez à faire, pas besoin de redéfinir le comportement de la pile arrière ...

16
Nemanja Kovacevic

Je sais que c'est une vieille question mais j'ai le même problème et je le répare comme ceci:

Commencez par ajouter Fragment1 à BackStack avec un nom (par exemple, "Frag1"):

frag = new Fragment1();

transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.addToBackStack("Frag1");
transaction.commit();

Et ensuite, chaque fois que vous souhaitez revenir à Fragment1 (même après avoir ajouté 10 fragments au-dessus), appelez simplement popBackStackImmediate avec le nom suivant:

getSupportFragmentManager().popBackStackImmediate("Frag1", 0);

J'espère que ça va aider quelqu'un :)

13
Shirane85

Après la réponse de @Arvis, j’ai décidé de creuser encore plus profondément et j’ai écrit un article technique à ce sujet ici: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments- chevauchement-dû-à-backstack-cauchemar sous Android/

Pour les développeurs paresseux autour. Ma solution consiste à toujours ajouter les transactions au backstack et à effectuer un FragmentManager.popBackStackImmediate() supplémentaire en cas de besoin (automatiquement).

Le code contient très peu de lignes de code et, dans mon exemple, je voulais passer de C à A sans revenir à "B" si l'utilisateur ne s'enfonçait pas plus profondément dans le panier (par exemple, de C navigue vers D).

Par conséquent, le code joint fonctionnerait comme suit A -> B -> C (retour) -> A & A -> B -> C -> D (retour) -> C (retour) -> B (retour) -> A

où 

fm.beginTransaction().replace(R.id.content, new CFragment()).commit()

sont passés de "B" à "C" comme dans la question.

Ok, ok voici le code :)

public static void performNoBackStackTransaction(FragmentManager fragmentManager, String tag, Fragment fragment) {
  final int newBackStackLength = fragmentManager.getBackStackEntryCount() +1;

  fragmentManager.beginTransaction()
      .replace(R.id.content, fragment, tag)
      .addToBackStack(tag)
      .commit();

  fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {
      int nowCount = fragmentManager.getBackStackEntryCount();
      if (newBackStackLength != nowCount) {
        // we don't really care if going back or forward. we already performed the logic here.
        fragmentManager.removeOnBackStackChangedListener(this);

        if ( newBackStackLength > nowCount ) { // user pressed back
          fragmentManager.popBackStackImmediate();
        }
      }
    }
  });
}
5
Andrea Baccega

Si vous vous battez avec addToBackStack () & popBackStack (), utilisez simplement

FragmentTransaction ft =getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, new HomeFragment(), "Home");
ft.commit();`

Dans votre activité dans OnBackPressed (), recherchez le segment par tag puis faites votre travail

Fragment home = getSupportFragmentManager().findFragmentByTag("Home");

if (home instanceof HomeFragment && home.isVisible()) {
    // do you stuff
}

Pour plus d'informations https://github.com/DattaHujare/NavigationDrawer Je n'utilise jamais addToBackStack () pour gérer les fragments.

1
Dattu Hujare

J'ai eu un problème similaire où j'avais 3 fragments consécutifs dans la même Activity [M1.F0] -> [M1.F1] -> [M1.F2] suivi d'un appel à une nouvelle Activity [M2]. Si l'utilisateur appuyait sur un bouton dans [M2], je souhaitais revenir à [M1, F1] au lieu de [M1, F2], ce qui correspond déjà au comportement de la presse arrière.

Pour ce faire, je supprime [M1, F2], appelle show sur [M1, F1], valide la transaction, puis rajoute [M1, F2] en l'appelant par hide. Cela a supprimé la presse arrière supplémentaire qui aurait autrement été laissée.

// Remove [M1.F2] to avoid having an extra entry on back press when returning from M2
final FragmentTransaction ftA = fm.beginTransaction();
ftA.remove(M1F2Fragment);
ftA.show(M1F1Fragment);
ftA.commit();
final FragmentTransaction ftB = fm.beginTransaction();
ftB.hide(M1F2Fragment);
ftB.commit();

Salut Après avoir fait ce code: Je ne parviens pas à voir la valeur de Fragment2 en appuyant sur la touche Retour . Mon code:

FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.frame, f1);
ft.remove(f1);

ft.add(R.id.frame, f2);
ft.addToBackStack(null);

ft.remove(f2);
ft.add(R.id.frame, f3);

ft.commit();

@Override
    public boolean onKeyDown(int keyCode, KeyEvent event){

        if(keyCode == KeyEvent.KEYCODE_BACK){
            Fragment currentFrag =  getFragmentManager().findFragmentById(R.id.frame);
            FragmentTransaction transaction = getFragmentManager().beginTransaction();

            if(currentFrag != null){
                String name = currentFrag.getClass().getName();
            }
            if(getFragmentManager().getBackStackEntryCount() == 0){
            }
            else{
                getFragmentManager().popBackStack();
                removeCurrentFragment();
            }
       }
    return super.onKeyDown(keyCode, event);
   }

public void removeCurrentFragment()
    {
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        Fragment currentFrag =  getFragmentManager().findFragmentById(R.id.frame);

        if(currentFrag != null){
            transaction.remove(currentFrag);
        }
        transaction.commit();
    }
0
Iñaqui

executePendingTransactions(), commitNow() non travaillé (

Travaillé dans androidx (jetpack).

private final FragmentManager fragmentManager = getSupportFragmentManager();

public void removeFragment(FragmentTag tag) {
    Fragment fragmentRemove = fragmentManager.findFragmentByTag(tag.toString());
    if (fragmentRemove != null) {
        fragmentManager.beginTransaction()
                .remove(fragmentRemove)
                .commit();

        // fix by @Ogbe
        fragmentManager.popBackStackImmediate(tag.toString(), 
            FragmentManager.POP_BACK_STACK_INCLUSIVE);
    }
}
0
tim4dev

Je pense que, quand j'ai lu votre histoire, [3] se trouve également sur le tapis. Cela explique pourquoi vous le voyez clignoter.

La solution serait de ne jamais définir [3] sur la pile.

0
jdekeij