Separar Back Stack para cada pestaña en Android usando Fragmentos

158

Estoy tratando de implementar pestañas para la navegación en una aplicación de Android. Dado que TabActivity y ActivityGroup están en desuso, me gustaría implementarlo usando Fragments en su lugar.

Sé cómo configurar un fragmento para cada pestaña y luego cambiar fragmentos cuando se hace clic en una pestaña. Pero, ¿cómo puedo tener una pila separada para cada pestaña?

Por ejemplo, los Fragmentos A y B estarían en la Pestaña 1 y los Fragmentos C y D en la Pestaña 2. Cuando se inicia la aplicación, se muestra el Fragmento A y se selecciona la Pestaña 1. Luego, el Fragmento A podría reemplazarse por el Fragmento B. Cuando se selecciona la Pestaña 2, se debe mostrar el Fragmento C. Si se selecciona la Pestaña 1, se debe mostrar nuevamente el Fragmento B. En este punto, debería ser posible usar el botón Atrás para mostrar el Fragmento A.

Además, es importante que el estado de cada pestaña se mantenga cuando se gira el dispositivo.

BR Martin

mardah
fuente

Respuestas:

23

El marco actualmente no lo hará automáticamente. Tendrá que construir y administrar sus propias pilas para cada pestaña.

Para ser honesto, esto parece algo realmente cuestionable. No puedo imaginar que resulte en una interfaz de usuario decente: si la tecla de retroceso va a hacer cosas diferentes dependiendo de la pestaña que soy, especialmente si la tecla de retroceso también tiene su comportamiento normal de cerrar toda la actividad cuando está en la parte superior de la pila ... suena desagradable.

Si está tratando de construir algo así como una interfaz de usuario del navegador web, obtener una experiencia de usuario que sea natural para el usuario implicará muchos ajustes sutiles de comportamiento según el contexto, por lo que definitivamente tendrá que hacer su propia pila. gestión en lugar de confiar en alguna implementación predeterminada en el marco. Por ejemplo, intente prestar atención a cómo la tecla de retroceso interactúa con el navegador estándar en las diversas formas en que puede entrar y salir de él. (Cada "ventana" en el navegador es esencialmente una pestaña).

hackbod
fuente
77
No hagas eso. Y el marco es apenas inútil. No le brinda soporte automático para este tipo de cosas, lo que, como dije, no puedo imaginar que resulte en una experiencia decente para el usuario, excepto en situaciones muy especializadas en las que deberá controlar cuidadosamente el comportamiento de la espalda de todos modos.
hackbod
9
Este tipo de navegación, entonces tiene pestañas y jerarquía de páginas en cada pestaña, es muy común para las aplicaciones de iPhone, por ejemplo (puede consultar las aplicaciones App Store y iPod). Encuentro su experiencia de usuario bastante decente.
Dmitry Ryadnenko
13
Esto es una locura. El iPhone ni siquiera tiene un botón de retroceso. Hay demostraciones de API que muestran un código muy simple para implementar fragmentos en pestañas. La pregunta que se hizo fue sobre tener diferentes pilas de respaldo para cada pestaña, y mi respuesta es que el marco no proporciona esto automáticamente porque semánticamente, para lo que hace el botón de retroceso, lo más probable es que sea una experiencia de usuario horrible. Sin embargo, puede implementar fácilmente la semántica posterior si lo desea.
hackbod
44
Nuevamente, el iPhone no tiene un botón de retroceso, por lo que semánticamente no tiene un comportamiento de pila de respaldo como Android. Además, "es mejor seguir con las actividades y ahorrarme mucho tiempo" no tiene ningún sentido aquí, porque las actividades no le permiten poner pestañas de mantenimiento en una interfaz de usuario con sus propias pilas de respaldo diferentes; de hecho, la gestión de back stack de las actividades es menos flexible que la que proporciona el marco Fragment.
hackbod
22
@hackbod Estoy tratando de seguir tus puntos, pero he tenido problemas para implementar un comportamiento personalizado de back-stack. Me doy cuenta de que habiendo participado en el diseño de esto, tendrías una idea sólida de lo fácil que podría ser. ¿Es posible que exista una aplicación de demostración para el caso de uso del OP? fragmentos de backstacks dentro de cada FragmentActivity.
Richard Le Mesurier el
138

Llego terriblemente tarde a esta pregunta. Pero dado que este hilo ha sido muy informativo y útil para mí, pensé que sería mejor publicar mis dos peniques aquí.

Necesitaba un flujo de pantalla como este (un diseño minimalista con 2 pestañas y 2 vistas en cada pestaña),

tabA
    ->  ScreenA1, ScreenA2
tabB
    ->  ScreenB1, ScreenB2

Tenía los mismos requisitos en el pasado, y lo hice usando TabActivityGroup(que también estaba en desuso en ese momento) y Actividades. Esta vez quería usar Fragmentos.

Así es como lo hice.

1. Crear una clase de fragmento base

public class BaseFragment extends Fragment {
    AppMainTabActivity mActivity;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mActivity = (AppMainTabActivity) this.getActivity();
    }

    public void onBackPressed(){
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data){
    }
}

Todos los fragmentos en su aplicación pueden extender esta clase Base. Si desea utilizar fragmentos especiales como ListFragmentdebería crear una clase base para eso también. Tendrás claro el uso de onBackPressed()y onActivityResult()si lees la publicación completa.

2. Cree algunos identificadores de pestaña, accesibles desde cualquier lugar del proyecto

public class AppConstants{
    public static final String TAB_A  = "tab_a_identifier";
    public static final String TAB_B  = "tab_b_identifier";

    //Your other constants, if you have them..
}

nada que explicar aquí ...

3. Ok, Actividad de la pestaña principal: revise los comentarios en código.

public class AppMainFragmentActivity extends FragmentActivity{
    /* Your Tab host */
    private TabHost mTabHost;

    /* A HashMap of stacks, where we use tab identifier as keys..*/
    private HashMap<String, Stack<Fragment>> mStacks;

    /*Save current tabs identifier in this..*/
    private String mCurrentTab;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.app_main_tab_fragment_layout);

        /*  
         *  Navigation stacks for each tab gets created.. 
         *  tab identifier is used as key to get respective stack for each tab
         */
        mStacks             =   new HashMap<String, Stack<Fragment>>();
        mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
        mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());

        mTabHost                =   (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setOnTabChangedListener(listener);
        mTabHost.setup();

        initializeTabs();
    }


    private View createTabView(final int id) {
        View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
        ImageView imageView =   (ImageView) view.findViewById(R.id.tab_icon);
        imageView.setImageDrawable(getResources().getDrawable(id));
        return view;
    }

    public void initializeTabs(){
        /* Setup your tab icons and content views.. Nothing special in this..*/
        TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);
        mTabHost.setCurrentTab(-3);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
        mTabHost.addTab(spec);


        spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
        mTabHost.addTab(spec);
    }


    /*Comes here when user switch tab, or we do programmatically*/
    TabHost.OnTabChangeListener listener    =   new TabHost.OnTabChangeListener() {
      public void onTabChanged(String tabId) {
        /*Set current tab..*/
        mCurrentTab                     =   tabId;

        if(mStacks.get(tabId).size() == 0){
          /*
           *    First time this tab is selected. So add first fragment of that tab.
           *    Dont need animation, so that argument is false.
           *    We are adding a new fragment which is not present in stack. So add to stack is true.
           */
          if(tabId.equals(AppConstants.TAB_A)){
            pushFragments(tabId, new AppTabAFirstFragment(), false,true);
          }else if(tabId.equals(AppConstants.TAB_B)){
            pushFragments(tabId, new AppTabBFirstFragment(), false,true);
          }
        }else {
          /*
           *    We are switching tabs, and target tab is already has atleast one fragment. 
           *    No need of animation, no need of stack pushing. Just show the target fragment
           */
          pushFragments(tabId, mStacks.get(tabId).lastElement(), false,false);
        }
      }
    };


    /* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
    public void setCurrentTab(int val){
          mTabHost.setCurrentTab(val);
    }


    /* 
     *      To add fragment to a tab. 
     *  tag             ->  Tab identifier
     *  fragment        ->  Fragment to show, in tab identified by tag
     *  shouldAnimate   ->  should animate transaction. false when we switch tabs, or adding first fragment to a tab
     *                      true when when we are pushing more fragment into navigation stack. 
     *  shouldAdd       ->  Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
     *                      true in all other cases.
     */
    public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
      if(shouldAdd)
          mStacks.get(tag).push(fragment);
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      if(shouldAnimate)
          ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }


    public void popFragments(){
      /*    
       *    Select the second last fragment in current tab's stack.. 
       *    which will be shown after the fragment transaction given below 
       */
      Fragment fragment             =   mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);

      /*pop current fragment from stack.. */
      mStacks.get(mCurrentTab).pop();

      /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      ft.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }   


    @Override
    public void onBackPressed() {
        if(mStacks.get(mCurrentTab).size() == 1){
          // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
          finish();
          return;
        }

        /*  Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
         *  when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
         *  kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
         */
        ((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();

        /* Goto previous fragment in navigation stack of this tab */
            popFragments();
    }


    /*
     *   Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
     *  in that fragment, and called it from the activity. But couldn't resist myself.
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(mStacks.get(mCurrentTab).size() == 0){
            return;
        }

        /*Now current fragment on screen gets onActivityResult callback..*/
        mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
    }
}

4. app_main_tab_fragment_layout.xml (en caso de que alguien esté interesado).

<?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>

        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>

        <TabWidget
            android:id="@android:id/tabs"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>

    </LinearLayout>
</TabHost>

5. AppTabAFirstFragment.java (primer fragmento en la pestaña A, similar para todas las pestañas)

public class AppTabAFragment extends BaseFragment {
    private Button mGotoButton;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view       =   inflater.inflate(R.layout.fragment_one_layout, container, false);

        mGoToButton =   (Button) view.findViewById(R.id.goto_button);
        mGoToButton.setOnClickListener(listener);

        return view;
    }

    private OnClickListener listener        =   new View.OnClickListener(){
        @Override
        public void onClick(View v){
            /* Go to next fragment in navigation stack*/
            mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
        }
    }
}

Esta podría no ser la forma más pulida y correcta. Pero funcionó muy bien en mi caso. Además, solo tenía este requisito en modo vertical. Nunca tuve que usar este código en un proyecto que admite ambas orientaciones. Así que no puedo decir qué tipo de desafíos enfrento allí ...

EDITAR:

Si alguien quiere un proyecto completo, he enviado un proyecto de muestra a github .

Krishnabhadra
fuente
2
Almacenar datos para cada fragmento, recrear cada uno de ellos, reconstruir las pilas ... tanto trabajo para un simple cambio de orientación.
Michael Eilers Smith
3
@omegatai está completamente de acuerdo con usted. Todo el problema surge ya que Android no administra la pila para nosotros (lo que hace iOS y el cambio de orientación o pestaña con múltiples fragmentos es muy sencillo ) y lo que nos lleva de vuelta a la discusión original en esta Q / Un hilo. No es bueno volver a eso ahora ..
Krishnabhadra 05 de
1
@Renjith Esto se debe a que el fragmento se recrea cada vez que cambias de pestaña ... No pienses ni una sola vez que tu fragmento se reutiliza en el interruptor de pestaña. cuando cambio de la pestaña A a B, la pestaña A se libera de la memoria. Por lo tanto, guarde sus datos en la actividad y cada vez verifique si la actividad tiene datos antes de intentar obtenerlos del servidor.
Krishnabhadra
2
@ Krishnabhadra Ok, eso suena mucho mejor. Déjame corregir en caso de que me equivoque. Según su ejemplo, solo hay una actividad y, por lo tanto, un paquete. Cree instancias de adaptador en el BaseFragment (refiriéndose a su proyecto) y guarde los datos allí. Úselos siempre que se construya la vista.
Renjith
1
Lo tengo para trabajar. Muchas gracias. ¡Cargar todo el proyecto fue una buena idea! :-)
Vinay W
96

Tuvimos que implementar exactamente el mismo comportamiento que usted describe para una aplicación recientemente. Las pantallas y el flujo general de la aplicación ya estaban definidos, por lo que tuvimos que mantenerlo (es un clon de la aplicación iOS ...). Afortunadamente, logramos deshacernos de los botones de retroceso en pantalla :)

Pirateamos la solución usando una mezcla de TabActivity, FragmentActivities (estábamos usando la biblioteca de soporte para fragmentos) y Fragments. En retrospectiva, estoy bastante seguro de que no fue la mejor decisión de arquitectura, pero logramos que funcione. Si tuviera que hacerlo de nuevo, probablemente trataría de hacer una solución más basada en la actividad (sin fragmentos), o trataría de tener solo una Actividad para las pestañas y dejar que el resto sean vistas (que creo que son mucho más) reutilizable que las actividades en general).

Entonces, los requisitos eran tener algunas pestañas y pantallas anidables en cada pestaña:

tab 1
  screen 1 -> screen 2 -> screen 3
tab 2
  screen 4
tab 3
  screen 5 -> 6

etc ...

Entonces, diga: el usuario comienza en la pestaña 1, navega de la pantalla 1 a la pantalla 2 y luego a la pantalla 3, luego cambia a la pestaña 3 y navega de la pantalla 4 a 6; si vuelve a la pestaña 1, debería volver a ver la pantalla 3 y si presiona Atrás debería volver a la pantalla 2; Vuelve y está en la pantalla 1; cambie a la pestaña 3 y estará en la pantalla 6 nuevamente.

La actividad principal en la aplicación es MainTabActivity, que extiende TabActivity. Cada pestaña está asociada con una actividad, digamos ActivityInTab1, 2 y 3. Y luego cada pantalla será un fragmento:

MainTabActivity
  ActivityInTab1
    Fragment1 -> Fragment2 -> Fragment3
  ActivityInTab2
    Fragment4
  ActivityInTab3
    Fragment5 -> Fragment6

Cada ActivityInTab contiene solo un fragmento a la vez y sabe cómo reemplazar un fragmento por otro (más o menos lo mismo que un Grupo de gravedad). Lo bueno es que es bastante fácil mantener pilas separadas para cada pestaña de esta manera.

La funcionalidad para cada ActivityInTab era la misma: saber cómo navegar de un fragmento a otro y mantener una pila de reserva, por lo que colocamos eso en una clase base. Llamémoslo simplemente ActivityInTab:

abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.

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

    /**
     * Navigates to a new fragment, which is added in the fragment container
     * view.
     * 
     * @param newFragment
     */
    protected void navigateTo(Fragment newFragment) {
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();

        ft.replace(R.id.content, newFragment);

        // Add this transaction to the back stack, so when the user presses back,
        // it rollbacks.
        ft.addToBackStack(null);
        ft.commit();
    }

}

El activity_in_tab.xml es solo esto:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:isScrollContainer="true">
</RelativeLayout>

Como puede ver, el diseño de la vista para cada pestaña era el mismo. Eso es porque es solo un FrameLayout llamado contenido que contendrá cada fragmento. Los fragmentos son los que tienen la vista de cada pantalla.

Solo para los puntos de bonificación, también agregamos un pequeño código para mostrar un diálogo de confirmación cuando el usuario presiona Atrás y no hay más fragmentos a los que volver:

// In ActivityInTab.java...
@Override
public void onBackPressed() {
    FragmentManager manager = getSupportFragmentManager();
    if (manager.getBackStackEntryCount() > 0) {
        // If there are back-stack entries, leave the FragmentActivity
        // implementation take care of them.
        super.onBackPressed();
    } else {
        // Otherwise, ask user if he wants to leave :)
        showExitDialog();
    }
}

Eso es más o menos la configuración. Como puede ver, cada FragmentActivity (o simplemente Activity en Android> 3) se encarga de todo el apilamiento con su propio FragmentManager.

Una actividad como ActivityInTab1 será realmente simple, solo mostrará su primer fragmento (es decir, la pantalla):

public class ActivityInTab1 extends ActivityInTab {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        navigateTo(new Fragment1());
    }
}

Entonces, si un fragmento necesita navegar a otro fragmento, tiene que hacer un pequeño lanzamiento desagradable ... pero no es tan malo:

// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());

Así que eso es todo. Estoy bastante seguro de que esta no es una solución muy canónica (y sobre todo no muy buena), por lo que me gustaría preguntar a los desarrolladores experimentados de Android cuál sería un mejor enfoque para lograr esta funcionalidad, y si esto no es "cómo es hecho "en Android, agradecería que me señale algún enlace o material que explique cuál es la forma de Android de abordar esto (pestañas, pantallas anidadas en pestañas, etc.). Siéntase libre de desgarrar esta respuesta en los comentarios :)

Una señal de que esta solución no es muy buena es que recientemente tuve que agregar algunas funciones de navegación a la aplicación. Algún botón extraño que debería llevar al usuario de una pestaña a otra y a una pantalla anidada. Hacerlo programáticamente fue un dolor de cabeza, debido a los problemas de quién sabe quién y cómo lidiar con cuándo se fragmentan e inicializan fragmentos y actividades. Creo que hubiera sido mucho más fácil si esas pantallas y pestañas fueran solo Vistas en realidad.


Finalmente, si necesita sobrevivir a los cambios de orientación, es importante que sus fragmentos se creen usando setArguments / getArguments. Si configuras variables de instancia en los constructores de tus fragmentos, estarás jodido. Pero afortunadamente, eso es realmente fácil de solucionar: solo guarde todo en setArguments en el constructor y luego recupere esas cosas con getArguments en onCreate para usarlas.

epidemia
fuente
13
Gran respuesta, pero creo que muy pocos verán esto. Elegí exactamente el mismo camino (como puedes ver en la conversación en la respuesta anterior) y no estoy contento con él como tú. Creo que Google realmente arruinó estos fragmentos, ya que esta API no cubre los casos de uso más importantes. Otro problema con el que puede encontrarse es la imposibilidad de incrustar un fragmento en otro fragmento.
Dmitry Ryadnenko
Gracias por el comentario boulder. Sí, no podría estar más de acuerdo con la API de fragmentos. Ya me he encontrado con el problema de los fragmentos anidados (es por eso que elegimos el enfoque "reemplazar un fragmento con otro", jeje).
epidemian
1
He implementado esto a través de TODAS las actividades. No me gustó lo que obtuve y voy a probar Fragments. ¡Eso es lo opuesto a tu experiencia! Hay mucha implementación con Actividades para manejar el ciclo de vida de las vistas secundarias en cada pestaña y también para implementar su propio botón de retroceso. Además, no puede simplemente mantener una referencia a todas las vistas o explotará la memoria. Espero que Fragments: 1) Admita el ciclo de vida de los Fragments con una separación clara de la memoria y 2) ayude a implementar la funcionalidad del botón de retroceso. Además, si usa fragmentos para este proceso, ¿no será más fácil ejecutarlo en Tablets?
gregm
¿Qué sucede cuando el usuario cambia de pestaña? ¿Se elimina la pila de fragmentos? ¿Cómo asegurarse de que quede la pila?
gregm
1
@gregm Si vas a 1 pestaña <-> 1 actividad como lo hice, la pila para cada pestaña permanecerá cuando se cambien las pestañas porque las actividades se mantienen vivas; solo se pausan y se reanudan. No sé si hay una manera de hacer que las actividades se destruyan y se vuelvan a crear cuando las pestañas se cambian en un TabActivity. Sin embargo, si usted hace los fragmentos dentro de las actividades reemplazarse como he sugerido, que son destruidos (y re-crean cuando se abrió la backstack). Por lo tanto, tendrá como máximo un fragmento vivo por pestaña en cualquier momento.
epidemia
6

Almacenar referencias fuertes a fragmentos no es la forma correcta.

FragmentManager proporciona putFragment(Bundle, String, Fragment)y saveFragmentInstanceState(Fragment).

Cualquiera de los dos es suficiente para implementar un backstack.


Usando putFragment, en lugar de reemplazar un Fragmento, separa el antiguo y agrega el nuevo. Esto es lo que el marco hace a una transacción de reemplazo que se agrega al backstack. putFragmentalmacena un índice en la lista actual de Fragmentos activos y el Marco guarda esos Fragmentos durante los cambios de orientación.

La segunda forma, usando saveFragmentInstanceState, guarda todo el estado del fragmento en un paquete que le permite realmente eliminarlo, en lugar de separarlo. El uso de este enfoque hace que la pila posterior sea más fácil de manipular, ya que puede hacer estallar un Fragmento cuando lo desee.


Usé el segundo método para este caso de uso:

SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
               \                          /
                \------------------------/

No quiero que el usuario regrese a la pantalla Registrarse, desde la tercera, presionando el botón Atrás. También doy vuelta a las animaciones entre ellos (usando onCreateAnimation), para que las soluciones hacky no funcionen, al menos sin que el usuario note claramente que algo no está bien.

Este es un caso de uso válido para un backstack personalizado, que hace lo que el usuario espera ...

private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";

private MyBackStack mBackStack;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    if (state == null) {
        mBackStack = new MyBackStack();

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.add(R.id.act_base_frg_container, new SignInFragment());
        tr.commit();
    } else {
        mBackStack = state.getParcelable(STATE_BACKSTACK);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(STATE_BACKSTACK, mBackStack);
}

private void showFragment(Fragment frg, boolean addOldToBackStack) {
    final FragmentManager fm = getSupportFragmentManager();
    final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);

    FragmentTransaction tr = fm.beginTransaction();
    tr.replace(R.id.act_base_frg_container, frg);
    // This is async, the fragment will only be removed after this returns
    tr.commit();

    if (addOldToBackStack) {
        mBackStack.push(fm, oldFrg);
    }
}

@Override
public void onBackPressed() {
    MyBackStackEntry entry;
    if ((entry = mBackStack.pop()) != null) {
        Fragment frg = entry.recreate(this);

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.replace(R.id.act_base_frg_container, frg);
        tr.commit();

        // Pop it now, like the framework implementation.
        fm.executePendingTransactions();
    } else {
        super.onBackPressed();
    }
}

public class MyBackStack implements Parcelable {

    private final List<MyBackStackEntry> mList;

    public MyBackStack() {
        mList = new ArrayList<MyBackStackEntry>(4);
    }

    public void push(FragmentManager fm, Fragment frg) {
        push(MyBackStackEntry.newEntry(fm, frg);
    }

    public void push(MyBackStackEntry entry) {
        if (entry == null) {
            throw new NullPointerException();
        }
        mList.add(entry);
    }

    public MyBackStackEntry pop() {
        int idx = mList.size() - 1;
        return (idx != -1) ? mList.remove(idx) : null;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        final int len = mList.size();
        dest.writeInt(len);
        for (int i = 0; i < len; i++) {
            // MyBackStackEntry's class is final, theres no
            // need to use writeParcelable
            mList.get(i).writeToParcel(dest, flags);
        }
    }

    protected MyBackStack(Parcel in) {
        int len = in.readInt();
        List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
        for (int i = 0; i < len; i++) {
            list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
        }
        mList = list;
    }

    public static final Parcelable.Creator<MyBackStack> CREATOR =
        new Parcelable.Creator<MyBackStack>() {

            @Override
            public MyBackStack createFromParcel(Parcel in) {
                return new MyBackStack(in);
            }

            @Override
            public MyBackStack[] newArray(int size) {
                return new MyBackStack[size];
            }
    };
}

public final class MyBackStackEntry implements Parcelable {

    public final String fname;
    public final Fragment.SavedState state;
    public final Bundle arguments;

    public MyBackStackEntry(String clazz, 
            Fragment.SavedState state,
            Bundle args) {
        this.fname = clazz;
        this.state = state;
        this.arguments = args;
    }

    public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
        final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
        final String name = frg.getClass().getName();
        final Bundle args = frg.getArguments();
        return new MyBackStackEntry(name, state, args);
    }

    public Fragment recreate(Context ctx) {
        Fragment frg = Fragment.instantiate(ctx, fname);
        frg.setInitialSavedState(state);
        frg.setArguments(arguments);
        return frg;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(fname);
        dest.writeBundle(arguments);

        if (state == null) {
            dest.writeInt(-1);
        } else if (state.getClass() == Fragment.SavedState.class) {
            dest.writeInt(0);
            state.writeToParcel(dest, flags);
        } else {
            dest.writeInt(1);
            dest.writeParcelable(state, flags);
        }
    }

    protected MyBackStackEntry(Parcel in) {
        final ClassLoader loader = getClass().getClassLoader();
        fname = in.readString();
        arguments = in.readBundle(loader);

        switch (in.readInt()) {
            case -1:
                state = null;
                break;
            case 0:
                state = Fragment.SavedState.CREATOR.createFromParcel(in);
                break;
            case 1:
                state = in.readParcelable(loader);
                break;
            default:
                throw new IllegalStateException();
        }
    }

    public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
        new Parcelable.Creator<MyBackStackEntry>() {

            @Override
            public MyBackStackEntry createFromParcel(Parcel in) {
                return new MyBackStackEntry(in);
            }

            @Override
            public MyBackStackEntry[] newArray(int size) {
                return new MyBackStackEntry[size];
            }
    };
}
sergio91pt
fuente
2

Descargo de responsabilidad:


Creo que este es el mejor lugar para publicar una solución relacionada en la que he trabajado para un tipo similar de problema que parece ser bastante estándar en Android. No va a resolver el problema para todos, pero puede ayudar a algunos.


Si la diferencia principal entre sus fragmentos es solo los datos que los respaldan (es decir, no hay muchas diferencias de diseño grandes), entonces es posible que no necesite reemplazar el fragmento, sino simplemente intercambiar los datos subyacentes y actualizar la vista.

Aquí hay una descripción de un posible ejemplo para este enfoque:

Tengo una aplicación que usa ListViews. Cada elemento de la lista es un padre con cierto número de hijos. Cuando toca el elemento, se debe abrir una nueva lista con esos elementos secundarios, dentro de la misma pestaña ActionBar que la lista original. Estas listas anidadas tienen un diseño muy similar (algunos ajustes condicionales aquí y allá tal vez), pero los datos son diferentes.

Esta aplicación tiene varias capas de descendencia debajo de la lista principal inicial y es posible que tengamos o no datos del servidor cuando el usuario intente acceder a cierta profundidad más allá de la primera. Debido a que la lista se construye a partir de un cursor de base de datos, y los fragmentos usan un cargador de cursor y un adaptador de cursor para llenar la vista de lista con elementos de lista, todo lo que debe suceder cuando se registra un clic es:

1) Cree un nuevo adaptador con los campos apropiados 'a' y 'desde' que coincidan con las nuevas vistas de elementos que se agregan a la lista y las columnas devueltas por el nuevo cursor.

2) Configure este adaptador como el nuevo adaptador para ListView.

3) Cree un nuevo URI basado en el elemento en el que se hizo clic y reinicie el cargador de cursor con el nuevo URI (y proyección). En este ejemplo, el URI se asigna a consultas específicas con los argumentos de selección transmitidos desde la IU.

4) Cuando los nuevos datos se hayan cargado desde el URI, cambie el cursor asociado con el adaptador al nuevo cursor, y la lista se actualizará.

No hay backstack asociado con esto, ya que no estamos usando transacciones, por lo que tendrá que crear las suyas propias o reproducir las consultas a la inversa cuando salga de la jerarquía. Cuando intenté esto, las consultas fueron lo suficientemente rápidas como para volver a realizarlas en oNBackPressed () hasta que esté en la parte superior de la jerarquía, momento en el cual el marco vuelve a tomar el botón Atrás.

Si se encuentra en una situación similar, asegúrese de leer los documentos: http://developer.android.com/guide/topics/ui/layout/listview.html

http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html

¡Espero que esto ayude a alguien!

corte
fuente
En caso de que alguien esté haciendo esto, y también esté usando un SectionIndexer (como AlphabetIndexer), puede notar que después de reemplazar el adaptador, su desplazamiento rápido no funciona. Es un error desafortunado, pero reemplazar el adaptador, incluso con un nuevo indexador, no actualiza la lista de secciones utilizadas por FastScroll. Hay una solución alternativa, consulte: descripción del problema y solución alternativa
corte
2

Tuve exactamente el mismo problema e implementé un proyecto de código abierto de github que cubre la pestaña apilada, la navegación hacia atrás y hacia arriba y está bien probado y documentado:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Este es un marco simple y pequeño para pestañas de navegación y cambio de fragmentos y manejo de navegación hacia arriba y hacia atrás. Cada pestaña tiene su propia pila de fragmentos. Utiliza ActionBarSherlock y es compatible con el nivel de API 8.

Sebastian Baltes
fuente
2

Este es un problema complejo ya que Android solo maneja 1 back stack, pero esto es factible. Me llevó días crear una biblioteca llamada Tab Stacker que hace exactamente lo que estás buscando: un historial de fragmentos para cada pestaña. Es de código abierto y está completamente documentado, y se puede incluir fácilmente con gradle. Puede encontrar la biblioteca en github: https://github.com/smart-fun/TabStacker

También puede descargar la aplicación de muestra para ver que el comportamiento corresponde a sus necesidades:

https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp

Si tiene alguna pregunta, no dude en enviar un correo.

Arnaud SmartFun
fuente
2

Me gustaría sugerir mi propia solución en caso de que alguien esté buscando y quiera probar y elegir la mejor para sus necesidades.

https://github.com/drusak/tabactivity

El propósito de crear la biblioteca es bastante banal: impleméntelo como iPhone.

Las principales ventajas:

  • use la biblioteca android.support.design con TabLayout;
  • cada pestaña tiene su propia pila usando FragmentManager (sin guardar las referencias de los fragmentos);
  • soporte para enlaces profundos (cuando necesita abrir una pestaña específica y un nivel de fragmento específico en ella);
  • guardar / restaurar estados de pestañas;
  • métodos adaptativos del ciclo de vida de fragmentos en pestañas;
  • bastante fácil de implementar para sus necesidades.
Kasurd
fuente
Gracias, esto ha sido muy útil. Necesito usar ListFragments además de Fragments, así que dupliqué BaseTabFragment.java a BaseTabListFragment.java y tuve que extender ListFragment. Luego tuve que cambiar varias partes del código donde siempre se suponía que esperaba un BaseTabFragment. ¿Hay una mejor manera?
primehalo
Desafortunadamente, no pensé en ListFragment. Técnicamente es la solución correcta, pero requerirá comprobaciones de adición para TabFragment y su instancia de BaseTabListFragment. Otro enfoque para usar Fragment con ListView dentro (exactamente lo mismo que ListFragment implementado). Lo pensaré. Gracias por señalarme en eso!
Kasurd
1

Una solución simple:

Cada vez que cambie la pestaña / vista de raíz llame:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

Borrará la BackStack. Recuerde llamar a esto antes de cambiar el fragmento raíz.

Y agregue fragmentos con esto:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();

Tenga en cuenta que .addToBackStack(null)y transaction.add, por ejemplo, podría cambiarse con transaction.replace.

Morten Holmgaard
fuente
-1

Este hilo fue muy muy interesante y útil.
Gracias Krishnabhadra por su explicación y código, uso su código y mejoré un poco, permitiendo que las pilas, currentTab, etc ... persistan de cambiar la configuración (girando principalmente).
Probado en dispositivos reales 4.0.4 y 2.3.6, no probado en emulador

Cambio esta parte del código en "AppMainTabActivity.java", el resto permanece igual. Tal vez Krishnabhadra agregará esto en su código.

Recuperar datos en Crear:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.app_main_tab_fragment_layout);

    /*  
     *  Navigation stacks for each tab gets created..
     *  tab identifier is used as key to get respective stack for each tab
     */

  //if we are recreating this activity...
    if (savedInstanceState!=null) {
         mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
         mCurrentTab = savedInstanceState.getString("currentTab");
    }
    else {
    mStacks = new HashMap<String, Stack<Fragment>>();
    mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
    mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());

    }

    mTabHost = (TabHost)findViewById(android.R.id.tabhost);
    mTabHost.setup();

    initializeTabs();

  //set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
    mTabHost.setOnTabChangedListener(listener);
}

Guarde las variables y póngalas en Bundle:

 //Save variables while recreating
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable("stack", mStacks);
    outState.putString("currentTab", mCurrentTab);
    //outState.putInt("tabHost",mTabHost);
}

Si existe una CurrentTab anterior, configúrela, de lo contrario, cree una nueva Tab_A:

public void initializeTabs(){
    /* Setup your tab icons and content views.. Nothing special in this..*/
    TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);

    spec.setContent(new TabHost.TabContentFactory() {
        public View createTabContent(String tag) {
            return findViewById(R.id.realtabcontent);
        }
    });
    spec.setIndicator(createTabView(R.drawable.tab_a_state_btn));
    mTabHost.addTab(spec);


    spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
    spec.setContent(new TabHost.TabContentFactory() {
        public View createTabContent(String tag) {
            return findViewById(R.id.realtabcontent);
        }
    });
    spec.setIndicator(createTabView(R.drawable.tab_b_state_btn));
    mTabHost.addTab(spec);

//if we have non default Tab as current, change it
    if (mCurrentTab!=null) {
        mTabHost.setCurrentTabByTag(mCurrentTab);
    } else {
        mCurrentTab=AppConstants.TAB_A;
        pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
    }
}

Espero que esto ayude a otras personas.

Sulfkain
fuente
Esto está mal. Cuando se llama a onCreate con un paquete, esos fragmentos no serán los mismos que se mostrarán en la pantalla y estás filtrando los antiguos, a menos que estés usando setRetainInstance. Y si el ActivityManager "guarda" su Actividad, ya que un Fragmento no es Serializable ni Parcelable, cuando el usuario regrese a su Actividad, se bloqueará.
sergio91pt
-1

Recomendaría no utilizar backstack basado en HashMap> hay muchos errores en el modo "no mantener actividades". No restaurará correctamente el estado en caso de que esté profundamente en la pila de fragmentos. Y también se rastreará en un fragmento de mapa anidado (con excepción: Fragmento que no se encontró ninguna vista para ID). Coz HashMap> después de que la aplicación background \ foreground sea nula

Optimizo el código anterior para trabajar con la pila de fragmentos

Es inferior TabView

Actividad principal Clase

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;

import com.strikersoft.nida.R;
import com.strikersoft.nida.abstractActivity.BaseActivity;
import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;

public class TagsActivity extends BaseActivity {
    public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
    private TabHost mTabHost;
    private String mCurrentTab;

    public static final String TAB_TAGS = "TAB_TAGS";
    public static final String TAB_MAP = "TAB_MAP";
    public static final String TAB_SETTINGS = "TAB_SETTINGS";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
        getActionBar().hide();
        setContentView(R.layout.tags_activity);

        mTabHost = (TabHost) findViewById(android.R.id.tabhost);

        mTabHost.setup();

        if (savedInstanceState != null) {
            mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
            initializeTabs();
            mTabHost.setCurrentTabByTag(mCurrentTab);
            /*
            when resume state it's important to set listener after initializeTabs
            */
            mTabHost.setOnTabChangedListener(listener);
        } else {
            mTabHost.setOnTabChangedListener(listener);
            initializeTabs();
        }
    }

    private View createTabView(final int id, final String text) {
        View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
        ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
        imageView.setImageDrawable(getResources().getDrawable(id));
        TextView textView = (TextView) view.findViewById(R.id.tab_text);
        textView.setText(text);
        return view;
    }

    /*
    create 3 tabs with name and image
    and add it to TabHost
     */
    public void initializeTabs() {

        TabHost.TabSpec spec;

        spec = mTabHost.newTabSpec(TAB_TAGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
        mTabHost.addTab(spec);

        spec = mTabHost.newTabSpec(TAB_MAP);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
        mTabHost.addTab(spec);


        spec = mTabHost.newTabSpec(TAB_SETTINGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
        mTabHost.addTab(spec);

    }

    /*
    first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
    for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
    */
    TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
        public void onTabChanged(String tabId) {

            mCurrentTab = tabId;

            if (tabId.equals(TAB_TAGS)) {
                pushFragments(SearchFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_MAP)) {
                pushFragments(MapContainerFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_SETTINGS)) {
                pushFragments(SettingsFragment.getInstance(), false,
                        false, null);
            }

        }
    };

/*
Example of starting nested fragment from another fragment:

Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
                TagsActivity tAct = (TagsActivity)getActivity();
                tAct.pushFragments(newFragment, true, true, null);
 */
    public void pushFragments(Fragment fragment,
                              boolean shouldAnimate, boolean shouldAdd, String tag) {
        FragmentManager manager = getFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        if (shouldAnimate) {
            ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
                    R.animator.fragment_slide_left_exit,
                    R.animator.fragment_slide_right_enter,
                    R.animator.fragment_slide_right_exit);
        }
        ft.replace(R.id.realtabcontent, fragment, tag);

        if (shouldAdd) {
            /*
            here you can create named backstack for realize another logic.
            ft.addToBackStack("name of your backstack");
             */
            ft.addToBackStack(null);
        } else {
            /*
            and remove named backstack:
            manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
            or remove whole:
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
             */
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        ft.commit();
    }

    /*
    If you want to start this activity from another
     */
    public static void startUrself(Activity context) {
        Intent newActivity = new Intent(context, TagsActivity.class);
        newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(newActivity);
        context.finish();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putString(M_CURRENT_TAB, mCurrentTab);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onBackPressed(){
        super.onBackPressed();
    }
}

tags_activity.xml

<

?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>
        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:background="@drawable/bg_main_app_gradient"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
        <TabWidget
            android:id="@android:id/tabs"
            android:background="#EAE7E1"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>
    </LinearLayout>
</TabHost>

tags_icon.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tabsLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/bg_tab_gradient"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="contentDescription" >

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_marginTop="4dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView 
        android:id="@+id/tab_text"
        android:layout_marginBottom="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/tab_text_color"/>

</LinearLayout>

ingrese la descripción de la imagen aquí

Flinbor
fuente