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.