Problèmes avec la pile arrière de fragments Android

121

J'ai un énorme problème avec la façon dont la backstack de fragments Android semble fonctionner et je serais très reconnaissant pour toute aide qui est offerte.

Imaginez que vous avez 3 fragments

[1] [2] [3]

Je veux que l'utilisateur puisse naviguer [1] > [2] > [3]mais sur le chemin du 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 [2]introduit le fragment dans le détenteur du fragment défini en XML.

La réalité de cela semble que si je ne veux [2]pas réapparaître lorsque l'utilisateur appuie sur le bouton retour [3], je ne dois pas appeler addToBackStackla transaction qui montre le fragment [3]. Cela semble complètement contre-intuitif (peut-être venant du monde iOS).

Quoi qu'il en soit, si je le fais de cette façon, quand je pars [1] > [2]et que j'appuie, j'arrive [1]comme prévu.

Si j'y vais [1] > [2] > [3]et que j'appuie, je reviens à [1](comme prévu). Maintenant, le comportement étrange se produit lorsque j'essaye de sauter à [2]nouveau à partir de [1]. Tout d'abord [3]est brièvement affiché avant d' [2]entrer en vue. Si j'appuie en arrière à ce stade [3]s'affiche, et si j'appuie à nouveau, 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 pour 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"

        />



Mettre à jour C'est le code que j'utilise pour construire par nav heirarchy

    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

Chris Birch
la source
mais le fragment se chevauche, lorsque je rétropresse du fragment C au fragment A.
Priyanka
J'ai le même problème, comment résoudre ce problème?
Priyanka
référez-vous mes ans .. cela peut vous aider < stackoverflow.com/questions/14971780/… >
MD Khali

Réponses:

203

Explication: qu'est-ce qui se passe ici?

Si nous gardons à l'esprit que cela .replace()équivaut à ce .remove().add()que nous savons par la documentation:

Remplacez un fragment existant qui a été ajouté à un conteneur. C'est essentiellement la même chose que d'appeler remove(Fragment)tous les fragments actuellement ajoutés qui ont été ajoutés avec le même containerViewIdet ensuite add(int, Fragment, String)avec les mêmes arguments donnés ici.

alors ce qui se passe est comme ça (j'ajoute des nombres au frag 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 toutes les choses trompeuses commencent à se produire)

N'oubliez pas que .addToBackStack()c'est enregistrer uniquement la transaction et non le fragment comme lui-même! Alors maintenant, nous avons frag3sur 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

Envisagez de mettre FragmentManager.BackStackChangedListeneren œuvre pour surveiller les changements dans la pile arrière et appliquez votre logique dans la onBackStackChanged()méthode:

Arvis
la source
Bonne explication @arvis. Cependant, comment pouvons-nous empêcher ce comportement sans recourir à des moyens de piratage comme celui de DexterMoon, ou celui de Nemanja en utilisant popBackStack qui montre le fragment lors de la lecture de l'animation de transition?
momo du
@momo vous pouvez implémenter FragmentManager.BackStackChangedListenerpour surveiller les modifications apportées à la pile arrière. Surveillez toutes vos transactions avec onBackStackChanged()méthode et agissez si nécessaire: par ex. tracez le décompte des transactions dans BackStack; vérifier une transaction particulière par nom ( FragmentTransaction addToBackStack (String name)) etc.
Arvis
Merci de répondre. J'ai en fait essayé cela plus tôt dans la journée, enregistré l'auditeur et supprimé le fragment de onBackstackChange. Pendant la lecture de la transition éclatante, le fragment devient une zone vide blanche. Je suppose que la méthode est déclenchée lorsque le popping démarre l'animation et non à la fin ...
momo
4
Évitez d'utiliser des piles arrière! cela n'aide pas vraiment à l'efficacité globale! utilisez plain replace () ou mieux encore remove / add à chaque fois que vous voulez naviguer!
stack_ved
@Arvis peut u pls m'aider à obtenir le même problème .... nombre de piles 0 mais mon fragment est toujours visible?
Erum
33

Droite!!! après avoir beaucoup arraché les cheveux, j'ai enfin trouvé comment faire fonctionner correctement.

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

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

La prochaine étape consiste à remplacer la méthode onKeyDown et à supprimer le fragment actuel chaque fois que vous appuyez sur le bouton Précédent.

@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!

Chris Birch
la source
cela fonctionne, mais cela ne peut pas être utilisé dans mon projet car le fragment est rechargé! Aucune suggestion?
TharakaNirmana
Mais, si je fais cela. Je reçois un écran vide lorsque je reviens de C à A pour le fragment C.
Nigam Patro
Je soupçonne que la réponse de @Arvis devrait éclairer votre problème
Chris Birch
16

Tout d'abord merci @Arvis pour une explication révélatrice.

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

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

Actuellement, seul le fragment f1 est affiché.

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 le fragment f2, ce code vous amène au fragment f3.

f2 -> f3

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

Je ne suis pas sûr en lisant des documents si cela devrait fonctionner, cette méthode de transaction pop-up est dite asynchrone, et peut-être qu'une meilleure façon serait d'appeler popBackStackImmediate (). Mais pour 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();

Ici, il y aura en fait un bref retour à f1 avant de passer à f3, donc un léger problème.

C'est en fait tout ce que vous avez à faire, pas besoin de remplacer le comportement de la pile arrière ...

Nemanja Kovacevic
la source
6
mais le pépin, c'est un problème
Zyoo
Cela semble moins "hack-ey" que d'écouter les changements de BackStack. Merci.
ahaisting le
13

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

Tout d'abord, ajoutez 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 puis, chaque fois que vous voulez revenir à Fragment1 (même après avoir ajouté 10 fragments au-dessus), appelez simplement popBackStackImmediate avec le nom:

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

J'espère que cela aidera quelqu'un :)

Shirane85
la source
1
Très bonne réponse. J'ai dû utiliser getSupportFragmentManager (). PopBackStackImmediate ("Frag1", FragmentManager.POP_BACK_STACK_INCLUSIVE); pour le faire fonctionner pour mon cas d'utilisation
eliasbagley
1
Où dois-je mettre ce code ???? getSupportFragmentManager (). popBackStackImmediate ("Frag1", 0); Sur MainActivty ou onBackPress
pavel
ça ne fonctionne pas. :( c'est mon code, dans mon cas, le fragment se chevauche. il ouvre le fragment A, mais le segment A se chevauche sur le fragment B.
Priyanka
FragmentManager fragmentManager = getActivity (). GetSupportFragmentManager (); fragmentManager.popBackStack (FragmentA.class.getName (), FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction (); FragmentE fragmentE = nouveau FragmentE (); fragmentTransaction.replace (R.id.fragment_content, fragmentE, fragmentE.getClass (). getName ()); fragmentTransaction.commit ();
Priyanka
5

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-overlapping- en raison du cauchemar-backstack-dans-android /

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

Le code est très peu de lignes de code et, dans mon exemple, je voulais sauter de C à A sans revenir à "B" si l'utilisateur n'allait pas plus loin dans la backstack (ex 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

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

ont été émis 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();
        }
      }
    }
  });
}
Andrea Baccega
la source
1
obtenir un crash sur cette ligne fragmentManager.popBackStackImmediate (); erreur: java.lang.IllegalStateException: FragmentManager est déjà en train d'exécuter des transactions à com.example.myapplication.FragmentA $ 2.onBackStackChanged (FragmentA.java:43)
Priyanka
1

Si vous êtes aux prises avec addToBackStack () et popBackStack (), utilisez simplement

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

Dans votre activité dans OnBackPressed (), découvrez le segment par tag, puis faites vos affaires

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.

Dattu Hujare
la source
0

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

La solution serait de ne jamais mettre [3] sur la pile.

jdekeij
la source
Salut jdekei Merci pour votre contribution. Le problème est que je ne peux pas voir où j'ajoute [3] à la backstack. J'ai ajouté un autre morceau de code qui montre (par programmation) exactement la navigation que j'effectuais à l'aide de boutons.
Chris Birch
Cela m'aide aussi mais je n'utilise que removeCurFragment de votre code et un peu un autre vérificateur de fragments. La méthode de remplacement fonctionne pour moi très bien, peut-être, c'est s old method issue but now itbien. Merci
Viktor V.
0

J'ai eu un problème similaire où j'avais 3 fragments consécutifs dans le même Activity[M1.F0] -> [M1.F1] -> [M1.F2] suivi d'un appel à un nouveau Activity[M2]. Si l'utilisateur appuyait sur un bouton dans [M2], je voulais revenir à [M1, F1] au lieu de [M1, F2], ce que faisait déjà le comportement de contre-pression.

Afin d'accomplir ceci je supprime [M1, F2], appelle show sur [M1, F1], valide la transaction, puis ajoute [M1, F2] en l'appelant avec hide. Cela a supprimé la presse arrière supplémentaire qui aurait autrement été laissée pour compte.

// 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 suis pas en mesure de 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();
    }
Iñaqui
la source
0

executePendingTransactions(), commitNow()pas travaillé (

Travaillé sous 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);
    }
}
tim4dev
la source