160 votes

Separate Back Stack for each tab in Android using Fragments

Je suis en train d'essayer de mettre en place des onglets pour la navigation dans une application Android. Étant donné que TabActivity et ActivityGroup sont obsolètes, je voudrais l'implémenter en utilisant des Fragments à la place.

Je sais comment configurer un fragment pour chaque onglet et ensuite les basculer lorsque un onglet est cliqué. Mais comment puis-je avoir une pile de retour distincte pour chaque onglet ?

Par exemple, le Fragment A et B seraient sous l'Onglet 1 et le Fragment C et D sous l'Onglet 2. Lorsque l'application est lancée, le Fragment A est affiché et l'Onglet 1 est sélectionné. Ensuite, le Fragment A pourrait être remplacé par le Fragment B. Lorsque l'Onglet 2 est sélectionné, le Fragment C devrait être affiché. Si l'Onglet 1 est ensuite sélectionné, le Fragment B devrait une fois de plus être affiché. À ce stade, il devrait être possible d'utiliser le bouton de retour pour afficher le Fragment A.

De plus, il est important que l'état de chaque onglet soit maintenu lorsque l'appareil est en rotation.

Cordialement Martin

140voto

Krishnabhadra Points 18384

Je suis terriblement en retard sur cette question. Mais comme ce fil de discussion a été très instructif et utile pour moi, j'ai pensé que je ferais mieux de poster mes deux pence ici.

J'avais besoin d'un flux d'écran comme celui-ci (un design minimaliste avec 2 onglets et 2 vues dans chaque onglet),

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

J'ai eu les mêmes exigences dans le passé, et je l'ai fait en utilisant TabActivityGroup (qui était également déprécié à l'époque) et Activités. Cette fois, je voulais utiliser les Fragments.

Alors voilà comment je l'ai fait.

1. Créer une classe de fragment de 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){
    }
}

Tous les fragments de votre application peuvent étendre cette classe de base. Si vous voulez utiliser des fragments spéciaux comme ListFragment vous devez créer une classe de base pour cela aussi. Vous serez clair sur l'utilisation de onBackPressed() y onActivityResult() si vous lisez le post dans son intégralité..

2. Créer quelques identifiants d'onglets, accessibles partout dans le projet

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

Rien à expliquer ici

3. Ok, Activité de l'onglet principal - S'il vous plaît passez par les commentaires dans le code

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 (Au cas où quelqu'un serait intéressé.)

<?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 (Premier fragment dans l'onglet A, similaire pour tous les onglets)

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);
        }
    }
}

Ce n'est peut-être pas la manière la plus polie et la plus correcte. Mais elle a fonctionné à merveille dans mon cas. De plus, je n'avais cette exigence qu'en mode portrait. Je n'ai jamais eu à utiliser ce code dans un projet supportant les deux orientations. Je ne peux donc pas dire quel genre de défis j'y ai rencontré

EDIT :

Si quelqu'un veut un projet complet, j'ai poussé un exemple de projet à github .

0 votes

Avez-vous pensé à la gestion des changements d'orientation? Il n'est pas recommandé de sauvegarder les vues (fragments) dans le bundle savedInstanceState, mais je ne vois pas d'autre option...

0 votes

@omegatai n'a jamais eu à gérer le changement d'orientation dans aucun de nos projets jusqu'à présent. Mais mon approche serait 1) sauvegarder toutes mes données dans l'activité 2) effectuer la logique non-UI dans l'activité 3) Fragment à chaque fois créé à partir des données existantes présentes dans l'activité. Je ne sauvegarderais jamais l'ensemble de la vue dans savedInstanceState, ce serait suicidaire.

0 votes

Un autre problème survient lorsque l'utilisateur saisit du texte dans EditText et que la rotation se produit sans enregistrer ce texte dans une structure de données. Je sauvegarderais ces textes dans savedInstanceState et les afficherais ensuite dans l'orientation cible, pour éviter à l'utilisateur de devoir les saisir à nouveau.. Eh bien, je pourrais avoir à affronter davantage de défis, mais je devrai les surmonter au fur et à mesure.

98voto

epidemian Points 10113

Nous avons dû implémenter exactement le même comportement que vous décrivez pour une application récemment. Les écrans et le flux général de l'application étaient déjà définis, nous devions donc nous y tenir (c'est un clone d'application iOS...). Heureusement, nous avons réussi à nous débarrasser des boutons de retour à l'écran :)

Nous avons hacké la solution en utilisant un mélange de TabActivity, FragmentActivities (nous utilisions la bibliothèque de support pour les fragments) et Fragments. Avec du recul, je suis assez sûr que ce n'était pas la meilleure décision d'architecture, mais nous avons réussi à faire fonctionner la chose. Si je devais le refaire, je tenterais probablement une solution plus basée sur les activités (sans fragments), ou j'essaierais de n'avoir qu'une seule activité pour les onglets et de laisser tout le reste en tant que vues (que je trouve beaucoup plus réutilisables que les activités dans l'ensemble).

Donc, les exigences étaient d'avoir des onglets et des écrans imbriqués dans chaque onglet :

onglet 1
  écran 1 -> écran 2 -> écran 3
onglet 2
  écran 4
onglet 3
  écran 5 -> 6

etc...

Disons que l'utilisateur commence dans l'onglet 1, navigue de l'écran 1 à l'écran 2 puis à l'écran 3, puis passe à l'onglet 3 et navigue de l'écran 4 à 6 ; s'il revient à l'onglet 1, il devrait revoir l'écran 3 et s'il appuie sur Retour, il devrait revenir à l'écran 2 ; Retour à nouveau et il est à l'écran 1 ; passe à l'onglet 3 et il est à l'écran 6 à nouveau.

L'activité principale de l'application est MainTabActivity, qui étend TabActivity. Chaque onglet est associé à une activité, disons ActivityInTab1, 2 et 3. Ensuite, chaque écran sera un fragment :

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

Chaque ActivityInTab ne détient qu'un fragment à la fois, et sait comment remplacer un fragment par un autre (à peu près la même chose qu'un ActvityGroup). L'avantage est qu'il est assez facile de maintenir des piles de retour distinctes pour chaque onglet de cette manière.

La fonctionnalité pour chaque ActivityInTab était assez similaire : savoir comment naviguer d'un fragment à un autre et maintenir une pile de retour, donc nous avons mis cela dans une classe de base. Appelons-la simplement ActivityInTab :

classe abstraite ActivityInTab extends FragmentActivity { // FragmentActivity est juste une activité pour la bibliothèque de support.

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

    /**
     * Navigue vers un nouveau fragment, qui est ajouté dans le conteneur de fragments
     * vue.
     *
     * @param newFragment
     */
    protected void navigateTo(Fragment newFragment) {
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();

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

        // Ajoutez cette transaction à la pile de retour, donc lorsque l'utilisateur appuie sur Retour,
        // il revient en arrière.
        ft.addToBackStack(null);
        ft.commit();
    }

}

L'activity_in_tab.xml est juste ceci :

Comme vous pouvez le voir, la mise en page de vue pour chaque onglet était la même. C'est parce qu'il s'agit simplement d'un FrameLayout appelé contenu qui contiendra chaque fragment. Ce sont les fragments qui ont la vue de chaque écran.

Juste pour les points bonus, nous avons également ajouté un peu de code pour afficher une boîte de dialogue de confirmation lorsque l'utilisateur appuie sur Retour et qu'il n'y a plus de fragments vers lesquels revenir :

// Dans ActivityInTab.java...
@Override
public void onBackPressed() {
    FragmentManager manager = getSupportFragmentManager();
    if (manager.getBackStackEntryCount() > 0) {
        // S'il y a des entrées dans la pile de retour, laissez la mise en œuvre de FragmentActivity s'en charger.
        super.onBackPressed();
    } else {
        // Sinon, demandez à l'utilisateur s'il veut partir :)
        showExitDialog();
    }
}

C'est à peu près la configuration. Comme vous pouvez le constater, chaque FragmentActivity (ou simplement Activity dans Android >3) s'occupe de toute la gestion de la pile de retour avec son propre FragmentManager.

Une activité comme ActivityInTab1 sera vraiment simple, il montrera juste son premier fragment (c'est-à-dire écran) :

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

Ensuite, si un fragment a besoin de naviguer vers un autre fragment, il doit effectuer un petit cast un peu sale... mais ce n'est pas si mal :

// Dans Fragment1.java par exemple...
// Besoin de naviguer vers Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());

Voilà à peu près tout. Je suis assez sûr que ce n'est pas une solution très canonique (et surtout pas très bonne), donc j'aimerais demander aux développeurs d'Android expérimentés quelle serait une meilleure approche pour réaliser cette fonctionnalité, et si ce n'est pas "comme ça que ça se fait" dans Android, j'apprécierais si vous pouviez me diriger vers un lien ou un matériel qui explique la manière Android d'approcher cela (onglets, écrans imbriqués dans les onglets, etc). N'hésitez pas à critiquer cette réponse dans les commentaires :)

Comme signe que cette solution n'est pas très bonne, récemment j'ai dû ajouter une fonctionnalité de navigation à l'application. Un bouton bizarre qui devrait amener l'utilisateur d'un onglet à un autre et dans un écran imbriqué. Faire cela de manière programmatique a été un cauchemar, à cause de problèmes qui sait qui et de la gestion de quand les fragments et les activités sont réellement instanciés et initialisés. Je pense que cela aurait été beaucoup plus facile si ces écrans et onglets étaient simplement des Vues en réalité.


Enfin, si vous avez besoin de survivre à des changements d'orientation, il est important que vos fragments soient créés en utilisant setArguments/getArguments. Si vous définissez des variables d'instance dans les constructeurs de vos fragments, vous serez pris au dépourvu. Mais heureusement, c'est très facile à corriger : sauvegardez simplement tout dans setArguments dans le constructeur, puis récupérez ces choses avec getArguments dans onCreate pour les utiliser.

13 votes

Super réponse mais je pense que très peu de gens verront ça. J'ai choisi exactement le même chemin (comme vous pouvez le voir dans la conversation de la réponse précédente) et je ne suis pas content de ça, comme vous. Je pense que Google a vraiment merdé avec ces fragments car cette API ne couvre pas les principaux cas d'utilisation. Un autre problème auquel vous pourriez être confronté est l'impossibilité d'intégrer un fragment dans un autre fragment.

0 votes

Merci pour le commentaire Boulder. Oui, je suis tout à fait d'accord concernant l'API fragments. J'ai déjà rencontré le problème des fragments imbriqués (c'est pourquoi nous avons opté pour l'approche "remplacer un fragment par un autre" ahah).

1 votes

J'ai mis en œuvre ceci via TOUTES les activités. Je n'ai pas aimé ce que j'ai obtenu et je vais essayer les Fragments. C'est l'opposé de votre expérience! Il y a beaucoup de mises en œuvre avec les Activités pour gérer le cycle de vie des vues enfants dans chaque onglet et aussi pour implémenter votre propre bouton de retour. De plus, vous ne pouvez pas simplement conserver une référence à toutes les vues sinon vous allez faire exploser la mémoire. J'espère que les Fragments vont: 1) Soutenir le cycle de vie des Fragments avec une séparation claire de la mémoire et 2) aider à implémenter la fonctionnalité du bouton de retour. De plus, si vous utilisez des fragments pour ce processus, ne sera-t-il pas plus facile de s'exécuter sur les tablettes?

23voto

hackbod Points 55292

Le framework ne le fera pas automatiquement pour vous actuellement. Vous devrez construire et gérer vos propres piles arrière pour chaque onglet.

Pour être honnête, cela semble être une chose vraiment discutable à faire. Je ne peux pas imaginer que cela aboutisse à une IU décente - si la touche de retour doit faire des choses différentes en fonction de l'onglet sur lequel je me trouve, surtout si la touche de retour a également son comportement normal de fermeture de toute l'activité en haut de la pile... ça sonne mal.

Si vous essayez de construire quelque chose comme une IU de navigateur Web, pour obtenir une UX naturelle pour l'utilisateur, il faudra beaucoup de réglages subtils du comportement en fonction du contexte, donc vous devrez certainement gérer votre propre pile arrière au lieu de vous fier à une implémentation par défaut dans le framework. Par exemple, essayez de prêter attention à la façon dont la touche de retour interagit avec le navigateur standard dans les différentes façons d'y entrer et d'en sortir. (Chaque "fenêtre" dans le navigateur est essentiellement un onglet).

0 votes

Ne serait-il pas préférable d'utiliser FragmentActivity pour chaque onglet ? Puisque le framework de fragments est si inutile lorsqu'il s'agit de choses non triviales.

7 votes

Ne fais pas ça. Et le framework n'est presque jamais inutile. Il ne vous offre pas un support automatique pour ce genre de chose, ce qui, comme je l'ai dit, ne peut pas donner une bonne expérience utilisateur sauf dans des situations très spécialisées où vous devrez de toute façon bien contrôler le comportement de retour.

9 votes

Ce type de navigation, puis vous avez des onglets et une hiérarchie de pages sur chaque onglet est très courant pour les applications iPhone par exemple (vous pouvez vérifier l'App Store et les applications iPod). Je trouve leur expérience utilisateur tout à fait décente.

6voto

sergio91pt Points 665

Stocker des références fortes aux fragments n'est pas la bonne méthode.

FragmentManager fournit putFragment(Bundle, String, Fragment) et saveFragmentInstanceState(Fragment).

Utiliser l'un ou l'autre est suffisant pour implémenter une pile de retour arrière.


En utilisant putFragment, au lieu de remplacer un fragment, vous détachez l'ancien et ajoutez le nouveau. C'est ce que le framework fait pour une transaction de remplacement ajoutée à la pile de retour arrière. putFragment stocke un index vers la liste actuelle des fragments actifs et ces fragments sont enregistrés par le framework lors des changements d'orientation.

La deuxième méthode, en utilisant saveFragmentInstanceState, enregistre l'état complet du fragment dans un Bundle vous permettant de le supprimer vraiment, au lieu de le détacher. En utilisant cette approche, la manipulation de la pile de retour arrière est plus facile, car vous pouvez retirer un fragment quand vous le souhaitez.


J'ai utilisé la deuxième méthode pour ce cas d'utilisation :

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

Je ne veux pas que l'utilisateur revienne à l'écran d'inscription, à partir du troisième, en appuyant sur le bouton retour. Je fais également des animations de retournement entre eux (en utilisant onCreateAnimation), donc les solutions de contournement ne fonctionneront pas, du moins sans que l'utilisateur se rende clairement compte que quelque chose ne va pas.

C'est un cas d'utilisation valide pour une pile de retour arrière personnalisée, faisant ce que l'utilisateur attend...

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);
    // C'est asynchrone, le fragment ne sera supprimé qu'après le retour de cette instruction
    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();

        // Le retirer maintenant, comme l'implémentation du framework.
        fm.executePendingTransactions();
    } else {
        super.onBackPressed();
    }
}

public class MyBackStack implements Parcelable {

    private final List mList;

    public MyBackStack() {
        mList = new ArrayList(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++) {
            // La classe MyBackStackEntry est finale, il n'est pas
            // nécessaire d'utiliser writeParcelable
            mList.get(i).writeToParcel(dest, flags);
        }
    }

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

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

            @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 CREATOR =
        new Parcelable.Creator() {

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

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

6voto

tausiq Points 36

Cela peut être facilement réalisé avec ChildFragmentManager

Voici un article à ce sujet avec le projet associé. jetez un coup d'œil,

http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X