Problemas con la pila de respaldo de Android Fragment

121

Tengo un gran problema con la forma en que parece funcionar el backstack de fragmentos de Android y agradecería cualquier ayuda que se ofrezca.

Imagina que tienes 3 fragmentos

[1] [2] [3]

Quiero que el usuario pueda navegar [1] > [2] > [3]pero en el camino de regreso (presionando el botón Atrás) [3] > [1].

Como me hubiera imaginado, esto se lograría al no llamar addToBackStack(..)al crear la transacción que trae un fragmento [2]al titular del fragmento definido en XML.

La realidad de esto parece ser que si no quiero volver [2]a aparecer cuando el usuario presiona el botón Atrás [3], no debo llamar addToBackStacka la transacción que muestra un fragmento [3]. Esto parece completamente contra-intuitivo (quizás proveniente del mundo de iOS).

De todos modos, si lo hago de esta manera, cuando salgo [1] > [2]y presiono hacia atrás, regreso a[1] lo esperado.

Si voy [1] > [2] > [3]y luego presiono hacia atrás, salto de nuevo a [1](como se esperaba). Ahora el comportamiento extraño ocurre cuando intento saltar de [2]nuevo [1]. En primer lugar [3]se muestra brevemente antes de que aparezca [2]. Si presiono hacia atrás en este punto, [3]se muestra, y si presiono hacia atrás una vez más, la aplicación se cierra.

¿Alguien puede ayudarme a entender qué está pasando aquí?


Y aquí está el archivo xml de diseño para mi actividad principal:

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

        />



Actualización Este es el código que estoy usando para construir por jerarquía de navegación

    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]

Muchas gracias

Chris Birch
fuente
pero el fragmento se superpone, cuando presiono de nuevo del Fragmento C al fragmento A.
Priyanka
Tengo el mismo problema, ¿cómo lo solucionas?
Priyanka
refiera mis ans .. puede ser de ayuda < stackoverflow.com/questions/14971780/… >
MD Khali

Respuestas:

203

Explicación: ¿qué está pasando aquí?

Si tenemos en cuenta que .replace()es igual a lo .remove().add()que sabemos por la documentación:

Reemplace un fragmento existente que se agregó a un contenedor. Esto es esencialmente lo mismo que llamar remove(Fragment)a todos los fragmentos agregados actualmente que se agregaron con el mismo containerViewIdy luego add(int, Fragment, String)con los mismos argumentos dados aquí.

entonces lo que sucede es así (estoy agregando números al fragmento para que quede más claro):

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

(Aquí todas las cosas engañosas comienzan a suceder)

¡Recuerde que .addToBackStack()está guardando solo la transacción, no el fragmento como sí mismo! Así que ahora tenemos frag3en el diseño:

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

Solución posible

Considere implementar FragmentManager.BackStackChangedListenerpara observar los cambios en la pila posterior y aplicar su lógica en el onBackStackChanged()método:

Arvis
fuente
Buena explicación @arvis. Sin embargo, ¿cómo podemos evitar este comportamiento sin recurrir a formas extravagantes como la de DexterMoon o la de Nemanja con popBackStack que muestra el Fragmento mientras se reproduce la animación de transición?
momo
@momo que podría implementar FragmentManager.BackStackChangedListenerpara observar los cambios en la pila posterior. Monitoree todas sus transacciones con el onBackStackChanged()método y actúe según sea necesario: por ej. rastrear un recuento de transacciones en BackStack; comprobar transacción particular por nombre ( FragmentTransaction addToBackStack (String name)) etc.
Arvis
Gracias por responder. De hecho, lo intenté hoy más temprano, registré al oyente y eliminé el fragmento de onBackstackChange. Mientras se reproduce la transición, el fragmento se convierte en un área vacía blanca. Supongo que el método se activa cuando el estallido inicia la animación y no cuando termina ...
momo
44
¡Evita usar pilas traseras! ¡realmente no ayuda con la eficiencia general! ¡use replace () o incluso elimine / agregue cada vez que quiera navegar!
stack_ved
@Arvis, ¿pueden ayudarme? Estoy teniendo el mismo problema ... stack stack 0 pero aún así mi fragmento es visible?
Erum
33

¡¡¡Correcto!!! Después de tirar mucho del cabello, finalmente he descubierto cómo hacer que esto funcione correctamente.

¡Parece que el fragmento [3] no se elimina de la vista cuando se presiona hacia atrás, por lo que debe hacerlo manualmente!

En primer lugar, no use replace () sino que use remove y add por separado. Parece que replace () no funciona correctamente.

La siguiente parte de esto es anular el método onKeyDown y eliminar el fragmento actual cada vez que se presiona el botón Atrás.

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

}

¡Espero que esto ayude!

Chris Birch
fuente
esto funciona, pero esto no se puede usar en mi proyecto ya que el fragmento se vuelve a cargar. ¿alguna sugerencia?
TharakaNirmana
Pero, si estoy haciendo esto. Recibo una pantalla en blanco cuando regreso de C a A para el Fragmento C.
Nigam Patro
Sospecho que la respuesta de @Arvis debería arrojar algo de luz sobre su problema
Chris Birch
16

En primer lugar, gracias @Arvis por una explicación reveladora.

Prefiero una solución diferente a la respuesta aceptada aquí para este problema. No me gusta jugar con anular el comportamiento de retroceso más de lo absolutamente necesario y cuando he intentado agregar y eliminar fragmentos por mi cuenta sin que aparezca la pila de retroceso predeterminada al presionar el botón de retroceso, me encontré en el infierno de fragmentos :) Si usted. agregue f2 sobre f1 cuando lo elimine f1 no llamará a ninguno de los métodos de devolución de llamada como onResume, onStart, etc. y eso puede ser muy desafortunado.

De todos modos, así es como lo hago:

Actualmente, solo se muestra el fragmento f1.

f1 -> f2

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

Nada fuera de lo común aquí. Que en el fragmento f2 este código te lleva a fragmentar f3.

f2 -> f3

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

No estoy seguro al leer documentos si esto debería funcionar, se dice que este método de transacción emergente es asíncrono, y tal vez una mejor manera sería llamar a popBackStackImmediate (). Pero hasta donde puedo decir en mis dispositivos está funcionando sin problemas.

La alternativa mencionada sería:

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

Aquí en realidad habrá un breve regreso a f1 antes de pasar a f3, por lo que hay un pequeño problema allí.

Esto es realmente todo lo que tiene que hacer, no es necesario anular el comportamiento de la pila de respaldo ...

Nemanja Kovacevic
fuente
66
pero la falla, eso es un problema
Zyoo
Esto parece menos "hack-ey" que escuchar los cambios de BackStack. Gracias.
ahaisting
13

Sé que es una pregunta antigua, pero tengo el mismo problema y lo soluciono así:

Primero, agregue Fragment1 a BackStack con un nombre (por ejemplo, "Frag1"):

frag = new Fragment1();

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

Y luego, cada vez que desee volver al Fragmento1 (incluso después de agregar 10 fragmentos encima), simplemente llame a popBackStackImmediate con el nombre:

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

Espero que ayude a alguien :)

Shirane85
fuente
1
Gran respuesta. Tuve que usar getSupportFragmentManager (). PopBackStackImmediate ("Frag1", FragmentManager.POP_BACK_STACK_INCLUSIVE); para que funcione para mi caso de uso sin embargo
eliasbagley
1
¿Dónde tengo que poner este código? getSupportFragmentManager (). popBackStackImmediate ("Frag1", 0); En MainActivty o onBackPress
pavel
no funciona. :( este es mi código, en mi caso Fragment Overlaping. abre el Fragment A, pero Frgment A se superpone al 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

Después de la respuesta de @Arvis, decidí profundizar aún más y he escrito un artículo técnico sobre esto aquí: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments-overlapping- debido a backstack-nightmare-in-android /

Para los desarrolladores perezosos de todo. Mi solución consiste en agregar siempre las transacciones al backstack y realizar un extraFragmentManager.popBackStackImmediate() cuando sea necesario (automáticamente).

El código tiene muy pocas líneas de código y, en mi ejemplo, quería saltar de C a A sin saltar de nuevo a "B" si el usuario no fue más profundo en el backstack (por ejemplo, de C navega a D).

Por lo tanto, el código adjunto funcionaría de la siguiente manera A -> B -> C (atrás) -> A y A -> B -> C -> D (atrás) -> C (atrás) -> B (atrás) -> A

dónde

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

fueron emitidos de "B" a "C" como en la pregunta.

Ok, Ok aquí está el código :)

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
fuente
1
se bloquea en esta línea fragmentManager.popBackStackImmediate (); error: java.lang.IllegalStateException: FragmentManager ya está ejecutando transacciones en com.example.myapplication.FragmentA $ 2.onBackStackChanged (FragmentA.java:43)
Priyanka
1

Si está luchando con addToBackStack () y popBackStack (), simplemente use

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

En su actividad en OnBackPressed (), descubra el segmento por etiqueta y luego haga sus cosas

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

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

Para obtener más información https://github.com/DattaHujare/NavigationDrawer , nunca uso addToBackStack () para manejar fragmentos.

Dattu Hujare
fuente
0

Creo que, cuando leí tu historia, [3] también está en el backstack. Esto explica por qué lo ves parpadear.

La solución sería nunca establecer [3] en la pila.

jdekeij
fuente
Hola jdekei Gracias por tu aporte. El problema es que no puedo ver dónde estoy agregando [3] al backstack. He agregado otro fragmento de código que demuestra (programáticamente) exactamente la navegación que estaba realizando usando los botones.
Chris Birch
También me ayuda, pero solo uso removeCurFragment de su código y un poco otro verificador de fragmentos. El método de reemplazo me funciona bien, puede ser, eso s old method issue but now itestá bien. Gracias
Viktor V.
0

Tuve un problema similar en el que tenía 3 fragmentos consecutivos en el mismo Activity[M1.F0] -> [M1.F1] -> [M1.F2] seguido de una llamada a un nuevoActivity [M2]. Si el usuario presionó un botón en [M2], quería volver a [M1, F1] en lugar de [M1, F2], que es lo que ya hizo el comportamiento de la presión de retroceso.

Para lograr esto, elimino [M1, F2], llamo show en [M1, F1], confirmo la transacción y luego agrego [M1, F2] de nuevo llamándolo con hide. Esto eliminó la presión de retroceso adicional que de otro modo se habría quedado atrás.

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

Hola Después de hacer este código: no puedo ver el valor de Fragment2 al presionar la tecla Atrás. Mi código:

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

executePendingTransactions(), commitNow()no trabajado (

Trabajó en 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
fuente