Problemy z tylnym stosem fragmentów Androida

121

Mam ogromny problem ze sposobem, w jaki wydaje się działać backstack fragmentów Androida i byłbym bardzo wdzięczny za każdą oferowaną pomoc.

Wyobraź sobie, że masz 3 fragmenty

[1] [2] [3]

Chcę, aby użytkownik mógł nawigować, [1] > [2] > [3]ale w drodze powrotnej (naciskając przycisk Wstecz) [3] > [1].

Jak sobie wyobrażałem, byłoby to osiągnięte bez wywoływania addToBackStack(..)podczas tworzenia transakcji, która wprowadza fragment [2]do posiadacza fragmentu zdefiniowanego w XML.

W rzeczywistości wydaje mi się, że jeśli nie chcę [2]się pojawiać ponownie, gdy użytkownik naciśnie przycisk powrotu [3], nie mogę wywoływać addToBackStacktransakcji, która pokazuje fragment [3]. Wydaje się to całkowicie sprzeczne z intuicją (być może pochodzi ze świata iOS).

W każdym razie, jeśli zrobię to w ten sposób, kiedy wyjdę z [1] > [2]i wrócę, wrócę [1]zgodnie z oczekiwaniami.

Jeśli pójdę [1] > [2] > [3]i cofnę się, wskoczę z powrotem do [1](zgodnie z oczekiwaniami). Teraz dziwne zachowanie ma miejsce, gdy próbuję [2]ponownie przeskoczyć do [1]. Przede wszystkim [3]jest krótko wyświetlany, zanim [2]pojawi się w widoku. Jeśli naciśnę wstecz w tym momencie, [3]zostanie wyświetlony komunikat, a jeśli naciśnę ponownie, aplikacja zostanie zamknięta.

Czy ktoś może mi pomóc zrozumieć, co się tutaj dzieje?


A oto plik xml układu dla mojej głównej działalności:

<?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"

        />



Aktualizacja To jest kod, którego używam do tworzenia według heirarchii nawigacyjnej

    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]

Wielkie dzięki

Chris Birch
źródło
ale fragment nakłada się, kiedy cofam się z fragmentu C do fragmentu A.
Priyanka
mam ten sam problem, jak to naprawić?
Priyanka
skieruj moje odpowiedzi ... może ci pomóc < stackoverflow.com/questions/14971780/… >
MD Khali

Odpowiedzi:

203

Wyjaśnienie: o co tu chodzi?

Jeśli pamiętamy, że .replace()jest to równe .remove().add()temu, które znamy z dokumentacji:

Zastąp istniejący fragment, który został dodany do kontenera. Jest to zasadniczo to samo, co wywołanie remove(Fragment)wszystkich aktualnie dodanych fragmentów, które zostały dodane z tym samym, containerViewIda następnie add(int, Fragment, String)z tymi samymi argumentami podanymi tutaj.

to, co się dzieje, wygląda tak (dodaję liczby do fragmentu, aby było bardziej zrozumiałe):

// 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

(tutaj zaczynają się dziać wszystkie wprowadzające w błąd rzeczy)

Pamiętaj, że .addToBackStack()zapisuje tylko transakcję, a nie fragment jako siebie! Więc teraz mamy frag3na układzie:

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

Możliwe rozwiązanie

Rozważ wdrożenie, FragmentManager.BackStackChangedListeneraby obserwować zmiany w stosie wstecznym i zastosować swoją logikę w onBackStackChanged()metodzie:

Arvis
źródło
Dobre wyjaśnienie @arvis. Jak jednak możemy zapobiec takiemu zachowaniu bez uciekania się do hackerskich sposobów, takich jak ten z DexterMoon lub ten z Nemanja za pomocą popBackStack, który pokazuje fragment podczas odtwarzania animacji przejścia?
momo
@momo możesz zaimplementować, FragmentManager.BackStackChangedListeneraby obserwować zmiany w stosie tylnym. Monitoruj wszystkie swoje transakcje onBackStackChanged()metodą i działaj w razie potrzeby: np. śledzić liczbę transakcji w BackStack; sprawdź konkretną transakcję po nazwie ( FragmentTransaction addToBackStack (String name)) itd.
Arvis
Dzięki za odpowiedź. Właściwie spróbowałem tego wcześniej, zarejestrowałem słuchacza i usunąłem fragment z onBackstackChange. Podczas odtwarzania trzaskającego przejścia fragment staje się białym pustym obszarem. Chyba metoda jest odpalana, gdy popping uruchamia animację, a nie kiedy się kończy ...
momo
4
Unikaj używania tylnych stosów! to naprawdę nie pomaga w ogólnej wydajności! użyj zwykłego zamień () lub jeszcze lepiej usuń / dodaj za każdym razem, gdy chcesz nawigować!
stack_ved,
@ Arvis czy u pls mogą mi pomóc, otrzymuję ten sam problem ... liczba stosów 0, ale nadal mój fragment jest widoczny?
Erum
33

Dobrze!!! po wielu wyrywaniu włosów w końcu wymyśliłem, jak to działa.

Wygląda na to, że fragment [3] nie jest usuwany z widoku po naciśnięciu przycisku wstecz, więc musisz to zrobić ręcznie!

Przede wszystkim nie używaj replace (), ale zamiast tego użyj usuń i dodaj oddzielnie. Wygląda na to, że funkcja replace () nie działa poprawnie.

Następną częścią jest zastąpienie metody onKeyDown i usunięcie bieżącego fragmentu za każdym razem, gdy naciśnięty zostanie przycisk Wstecz.

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

}

Mam nadzieję że to pomoże!

Chris Birch
źródło
to działa, ale nie można tego użyć w moim projekcie, ponieważ fragment zostanie ponownie załadowany! jakieś sugestie?
TharakaNirmana,
Ale jeśli to robię. Kiedy wracam z punktu C do punktu A, po fragmencie C pojawia się pusty ekran
Nigam Patro
Podejrzewam, że odpowiedź @Arvis powinna rzucić trochę światła na twój problem
Chris Birch
16

Przede wszystkim dziękuję @Arvis za otwierające oczy wyjaśnienie.

Wolę inne rozwiązanie niż zaakceptowana tutaj odpowiedź na ten problem. Nie lubię majstrować przy zastępowaniu zachowania wstecz, bardziej niż jest to absolutnie konieczne, a kiedy próbowałem samodzielnie dodawać i usuwać fragmenty bez domyślnego wyskakującego stosu po naciśnięciu przycisku Wstecz, znalazłem się w piekle fragmentów :) Jeśli ty. dodaj f2 do f1, gdy go usuniesz, f1 nie wywoła żadnej z metod wywołania zwrotnego, takich jak onResume, onStart itp. i może to być bardzo niefortunne.

W każdym razie tak to robię:

Obecnie na wyświetlaczu jest tylko fragment f1.

f1 -> f2

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

nic niezwykłego tutaj. Następnie we fragmencie f2 ten kod prowadzi do fragmentu f3.

f2 -> f3

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

Nie jestem pewien, czy czytając dokumentację, czy to powinno zadziałać, mówi się, że ta metoda transakcji poping jest asynchroniczna i być może lepszym sposobem byłoby wywołanie popBackStackImmediate (). Ale o ile mogę stwierdzić, na moich urządzeniach działa bez zarzutu.

Wspomniana alternatywa to:

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

Tutaj faktycznie będzie krótki powrót do f1 przed przejściem do f3, więc tam drobny błąd.

To właściwie wszystko, co musisz zrobić, nie ma potrzeby zastępowania zachowania stosu wstecznego ...

Nemanja Kovacevic
źródło
6
ale usterka, to jest problem
Zyoo
Wydaje się to mniej "hack-ey" niż nasłuchiwanie zmian w BackStack. Dzięki.
ahaisting
13

Wiem, że to stara kwestia, ale mam ten sam problem i naprawię go w ten sposób:

Najpierw dodaj Fragment1 do BackStack z nazwą (np. „Frag1”):

frag = new Fragment1();

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

A potem, ilekroć chcesz wrócić do Fragmentu1 (nawet po dodaniu 10 fragmentów nad nim), po prostu wywołaj popBackStackImmediate z nazwą:

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

Mam nadzieję, że to komuś pomoże :)

Shirane85
źródło
1
Świetna odpowiedź. Musiałem użyć getSupportFragmentManager (). PopBackStackImmediate ("Frag1", FragmentManager.POP_BACK_STACK_INCLUSIVE); aby to działało w moim przypadku użycia
eliasbagley
1
Gdzie mam umieścić ten kod ???? getSupportFragmentManager (). popBackStackImmediate ("Frag1", 0); On MainActivty lub onBackPress
pavel
to nie działa. :( to jest mój kod, w moim przypadku Fragment się nakłada. otwiera fragment A, ale fragment A nakłada się na fragment B.
Priyanka
FragmentManager fragmentManager = getActivity (). GetSupportFragmentManager (); fragmentManager.popBackStack (FragmentA.class.getName (), FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction (); FragmentE fragmentE = new FragmentE (); fragmentTransaction.replace (R.id.fragment_content, fragmentE, fragmentE.getClass (). getName ()); fragmentTransaction.commit ();
Priyanka
5

Po odpowiedzi @Arvis postanowiłem poszukać jeszcze głębiej i napisałem artykuł techniczny na ten temat tutaj: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments-overlapping- z powodu koszmaru-backstack-in-android /

Dla leniwych programistów dookoła. Moje rozwiązanie polega na zawsze dodawaniu transakcji do backstacka i wykonywaniu dodatkowychFragmentManager.popBackStackImmediate() gdy jest to potrzebne (automatycznie).

Kod składa się z bardzo niewielu wierszy kodu, aw moim przykładzie chciałem przeskoczyć z C do A bez przeskakiwania z powrotem do „B”, jeśli użytkownik nie wszedł głębiej w backstack (np. Z C przechodzi do D).

Stąd załączony kod działałby następująco A -> B -> C (tył) -> A & A -> B -> C -> D (tył) -> C (tył) -> B (tył) -> A

gdzie

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

zostały wydane od „B” do „C” jak w pytaniu.

Ok, ok tu jest kod :)

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
źródło
1
uzyskiwanie awarii w tej linii fragmentManager.popBackStackImmediate (); błąd: java.lang.IllegalStateException: FragmentManager wykonuje już transakcje pod adresem com.example.myapplication.FragmentA 2 $ onBackStackChanged (FragmentA.java:43)
Priyanka
1

Jeśli zmagasz się z addToBackStack () i popBackStack (), po prostu użyj

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

W swojej aktywności w OnBackPressed () znajdź fargowanie według tagu, a następnie rób swoje

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

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

Aby uzyskać więcej informacji https://github.com/DattaHujare/NavigationDrawer Nigdy nie używam addToBackStack () do obsługi fragmentu.

Dattu Hujare
źródło
0

Myślę, że kiedy czytam twoją historię, [3] jest również na zapleczu. To wyjaśnia, dlaczego widzisz, jak miga.

Rozwiązaniem byłoby nigdy nie umieszczać [3] na stosie.

jdekeij
źródło
Cześć jdekei Dzięki za wkład. Problem polega na tym, że nie widzę, gdzie dodam [3] do backstacka. Dodałem kolejny fragment kodu, który demonstruje (programowo) dokładnie nawigację, którą wykonywałem za pomocą przycisków.
Chris Birch,
Mnie też to pomaga, ale używam tylko removeCurFragment z twojego kodu i trochę innego sprawdzania fragmentów. Metoda zamiany działa dla mnie dobrze, może być w s old method issue but now itporządku. Dzięki
Viktor V.
0

Miałem podobny problem, gdzie miałem 3 kolejne fragmenty w tym samym Activity[M1.F0] -> [M1.F1] -> [M1.F2], po których następowało wezwanie do nowegoActivity [M2]. Jeśli użytkownik nacisnął przycisk w [M2], chciałem wrócić do [M1, F1] zamiast do [M1, F2], co już robiło zachowanie wstecznego naciśnięcia.

W tym celu usuwam [M1, F2], wywołuję show na [M1, F1], zatwierdzam transakcję, a następnie dodaję z powrotem [M1, F2], wywołując ją z użyciem funkcji hide. Spowodowało to usunięcie dodatkowej prasy wstecznej, która w przeciwnym razie zostałaby pozostawiona.

// 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();

Cześć Po wykonaniu tego kodu: Nie mogę zobaczyć wartości Fragment2 po naciśnięciu klawisza Wstecz. Mój kod:

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
źródło
0

executePendingTransactions() , commitNow() nie pracował (

Pracował w systemie 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
źródło