Android Jetpack Navigation, BottomNavigationView avec fragment automatique de pile arrière lors du clic du bouton de retour?
Ce que je voulais, après avoir choisi plusieurs onglets, l’un après l’autre, par utilisateur et l’utilisateur cliquant sur le bouton Précédent, l’application doit être redirigé vers la dernière page ouverte.
J'ai obtenu le même résultat avec Android ViewPager, en enregistrant l'élément actuellement sélectionné dans une liste de tableaux. Existe-t-il une pile de retour automatique après la version de navigation Android Jetpack? Je veux y arriver en utilisant un graphe de navigation
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:id="@+id/container"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
Android:id="@+id/my_nav_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/navigation"
Android:layout_width="0dp"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
Android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</Android.support.constraint.ConstraintLayout>
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@+id/navigation_home"
Android:icon="@drawable/ic_home"
Android:title="@string/title_home" />
<item
Android:id="@+id/navigation_people"
Android:icon="@drawable/ic_group"
Android:title="@string/title_people" />
<item
Android:id="@+id/navigation_organization"
Android:icon="@drawable/ic_organization"
Android:title="@string/title_organization" />
<item
Android:id="@+id/navigation_business"
Android:icon="@drawable/ic_business"
Android:title="@string/title_business" />
<item
Android:id="@+id/navigation_tasks"
Android:icon="@drawable/ic_dashboard"
Android:title="@string/title_tasks" />
</menu>
également ajouté
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_Host_fragment))
J'ai reçu une réponse de Levi Moreira
, comme suit
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_Host_fragment))
}
Mais en faisant cela, la seule instance de ce dernier fragment ouvert est à nouveau créée.
Fournir une navigation arrière appropriée pour BottomNavigationView
Vous n'avez pas vraiment besoin d'une ViewPager
pour travailler avec BottomNavigation
et le nouveau composant d'architecture de navigation. Je travaille dans un exemple d'application qui utilise exactement les deux, voir ici .
Le concept de base est le suivant: vous avez l’activité principale qui hébergera la BottomNavigationView
et c’est l’hôte de navigation de votre graphe de navigation. Voici comment le code xml se présente:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:id="@+id/container"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
Android:id="@+id/my_nav_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/navigation"
Android:layout_width="0dp"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
Android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</Android.support.constraint.ConstraintLayout>
Le menu de navigation (menu des onglets) pour la BottomNavigationView
se présente comme suit:
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@+id/navigation_home"
Android:icon="@drawable/ic_home"
Android:title="@string/title_home" />
<item
Android:id="@+id/navigation_people"
Android:icon="@drawable/ic_group"
Android:title="@string/title_people" />
<item
Android:id="@+id/navigation_organization"
Android:icon="@drawable/ic_organization"
Android:title="@string/title_organization" />
<item
Android:id="@+id/navigation_business"
Android:icon="@drawable/ic_business"
Android:title="@string/title_business" />
<item
Android:id="@+id/navigation_tasks"
Android:icon="@drawable/ic_dashboard"
Android:title="@string/title_tasks" />
</menu>
Tout cela n’est que la configuration BottomNavigationView
. Maintenant, pour que cela fonctionne avec le composant Navigation Arch, vous devez accéder à l'éditeur de graphique de navigation, ajouter toutes vos destinations de fragments (dans mon cas, j'en ai 5, un pour chaque onglet) et définir l'identifiant de la destination avec le même nom comme celui du fichier navigation.xml
:
Cela dira à Android de faire un lien entre l'onglet et le fragment, chaque fois que l'utilisateur clique sur l'onglet "Accueil", Android s'occupe de charger le fragment correct . Il existe également un morceau de code kotlin qui nécessite à ajouter à votre NavHost (l'activité principale) pour relier les éléments avec la variable BottomNavigationView
:
Vous devez ajouter dans votre onCreate:
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_Host_fragment))
Cela indique à Android de faire le câblage entre le composant d'architecture de navigation et le BottomNavigationView. Voir plus dans la docs .
Pour obtenir le même statut que vous avez lorsque vous utilisez YouTube, ajoutez simplement ceci:
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_Host_fragment))
}
Cela fera en sorte que les destinations se retrouvent dans le panier. Ainsi, lorsque vous appuierez sur le bouton Retour, la dernière destination visitée sera affichée.
Vous devez définir la navigation hôte comme ci-dessous xml:
<LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:orientation="vertical">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:background="@color/colorPrimary" />
<fragment
Android:id="@+id/navigation_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
Android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/bottom_navigation_view"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
app:itemIconTint="@drawable/color_state_list"
app:itemTextColor="@drawable/color_state_list"
app:menu="@menu/menu_bottom_navigation" />
</LinearLayout>
Configuration avec contrôleur de navigation:
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.navigation_Host_fragment);
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.getNavController());
menu_bottom_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@id/tab1" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab1" />
<item
Android:id="@id/tab2" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab2" />
<item
Android:id="@id/tab3" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab3" />
</menu>
nav_graph.xml:
<navigation xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:id="@+id/nav_graph"
app:startDestination="@id/tab1">
<fragment
Android:id="@+id/tab1"
Android:name="com.navigationsample.Tab1Fragment"
Android:label="@string/tab1"
tools:layout="@layout/fragment_tab_1" />
<fragment
Android:id="@+id/tab2"
Android:name="com.navigationsample.Tab2Fragment"
Android:label="@string/tab2"
tools:layout="@layout/fragment_tab_2"/>
<fragment
Android:id="@+id/tab3"
Android:name="com.simform.navigationsample.Tab3Fragment"
Android:label="@string/tab3"
tools:layout="@layout/fragment_tab_3"/>
</navigation>
En configurant le même identifiant de "nav_graph" sur "menu_bottom_navigation", vous pourrez gérer le clic de navigation en bas.
Vous pouvez gérer une action en retour en utilisant la propriété popUpTo
dans la balise action
.
Vous pouvez avoir une configuration de viewpager avec une vue de navigation inférieure. Chaque fragment dans le viewpager sera un fragment de conteneur, il aura des fragments enfants avec son propre backstack. Vous pouvez conserver le backstack pour chaque onglet dans Viewpager de cette façon
J'ai créé une application comme celle-ci (qui n'est toujours pas publiée sur PlayStore) qui offre la même navigation. Peut-être que son implémentation est différente de celle de Google dans ses applications, mais les fonctionnalités sont les mêmes.
la structure implique que j'ai pour activité principale de changer de contenu en affichant/masquant des fragments à l'aide de:
public void switchTo(final Fragment fragment, final String tag /*Each fragment should have a different Tag*/) {
// We compare if the current stack is the current fragment we try to show
if (fragment == getSupportFragmentManager().getPrimaryNavigationFragment()) {
return;
}
// We need to hide the current showing fragment (primary fragment)
final Fragment currentShowingFragment = getSupportFragmentManager().getPrimaryNavigationFragment();
final FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
if (currentShowingFragment != null) {
fragmentTransaction.hide(currentShowingFragment);
}
// We try to find that fragment if it was already added before
final Fragment alreadyAddedFragment = getSupportFragmentManager().findFragmentByTag(tag);
if (alreadyAddedFragment != null) {
// Since its already added before we just set it as primary navigation and show it again
fragmentTransaction.setPrimaryNavigationFragment(alreadyAddedFragment);
fragmentTransaction.show(alreadyAddedFragment);
} else {
// We add the new fragment and then show it
fragmentTransaction.add(containerId, fragment, tag);
fragmentTransaction.show(fragment);
// We set it as the primary navigation to support back stack and back navigation
fragmentTransaction.setPrimaryNavigationFragment(fragment);
}
fragmentTransaction.commit();
}
Si vous avez une bottomNavigationView
avec 3 éléments correspondant à 3 Fragment
s: FragmentA
, FragmentB
et FragmentC
où FragmentA
est la startDestination
dans votre graphique de navigation, alors lorsque vous êtes sur FragmentB
ou FragmentC
et que vous cliquez en arrière, vous serez redirigé vers, FragmentA
, c'est le comportement recommandé par Google et mis en œuvre par défaut.
Si toutefois vous souhaitez modifier ce comportement, vous devrez soit utiliser une variable ViewPager
comme suggéré par d'autres réponses, soit gérer manuellement les fragments backStack et back transactions vous-même, ce qui, en quelque sorte, compromettrait l'utilisation du paramètre Navigation. composant tout à fait.
Tout d’abord, laissez-moi clarifier la manière dont Youtube et Instagram gèrent la navigation par fragments.
Aucune des réponses ci-dessus ne résout tous ces problèmes en utilisant la navigation jetpack.
La navigation dans JetPack n’a aucun moyen standard de le faire, la méthode que j’ai trouvée plus simple consiste à diviser le graphique xml de navigation en un pour chaque élément de navigation inférieur, en gérant moi-même la pile arrière entre les éléments de navigation à l’aide de l’activité FragmentManager et en utilisant le contrôleur de navigation JetPack pour gérer la navigation interne entre les fragments racine et de détail (son implémentation utilise la pile childFragmentManager).
Supposons que vous ayez dans votre dossier navigation
ces 3 xmls:
res/navigation/
navigation_feed.xml
navigation_explore.xml
navigation_profile.xml
Dans le fichier XML de navigation, vos ID destination sont identiques à ceux de vos ID de menu bottomNavigationBar. De plus, pour chaque fichier XML, définissez app:startDestination
sur le fragment que vous souhaitez utiliser comme racine de l'élément de navigation.
Créez une classe BottomNavController.kt
:
class BottomNavController(
val context: Context,
@IdRes val containerId: Int,
@IdRes val appStartDestinationId: Int
) {
private val navigationBackStack = BackStack.of(appStartDestinationId)
lateinit var activity: Activity
lateinit var fragmentManager: FragmentManager
private var listener: OnNavigationItemChanged? = null
private var navGraphProvider: NavGraphProvider? = null
interface OnNavigationItemChanged {
fun onItemChanged(itemId: Int)
}
interface NavGraphProvider {
@NavigationRes
fun getNavGraphId(itemId: Int): Int
}
init {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is Activity) {
activity = ctx
fragmentManager = (activity as FragmentActivity).supportFragmentManager
break
}
ctx = ctx.baseContext
}
}
fun setOnItemNavigationChanged(listener: (itemId: Int) -> Unit) {
this.listener = object : OnNavigationItemChanged {
override fun onItemChanged(itemId: Int) {
listener.invoke(itemId)
}
}
}
fun setNavGraphProvider(provider: NavGraphProvider) {
navGraphProvider = provider
}
fun onNavigationItemReselected(item: MenuItem) {
// If the user press a second time the navigation button, we pop the back stack to the root
activity.findNavController(containerId).popBackStack(item.itemId, false)
}
fun onNavigationItemSelected(itemId: Int = navigationBackStack.last()): Boolean {
// Replace fragment representing a navigation item
val fragment = fragmentManager.findFragmentByTag(itemId.toString())
?: NavHostFragment.create(navGraphProvider?.getNavGraphId(itemId)
?: throw RuntimeException("You need to set up a NavGraphProvider with " +
"BottomNavController#setNavGraphProvider")
)
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.replace(containerId, fragment, itemId.toString())
.addToBackStack(null)
.commit()
// Add to back stack
navigationBackStack.moveLast(itemId)
listener?.onItemChanged(itemId)
return true
}
fun onBackPressed() {
val childFragmentManager = fragmentManager.findFragmentById(containerId)!!
.childFragmentManager
when {
// We should always try to go back on the child fragment manager stack before going to
// the navigation stack. It's important to use the child fragment manager instead of the
// NavController because if the user change tabs super fast commit of the
// supportFragmentManager may mess up with the NavController child fragment manager back
// stack
childFragmentManager.popBackStackImmediate() -> {
}
// Fragment back stack is empty so try to go back on the navigation stack
navigationBackStack.size > 1 -> {
// Remove last item from back stack
navigationBackStack.removeLast()
// Update the container with new fragment
onNavigationItemSelected()
}
// If the stack has only one and it's not the navigation home we should
// ensure that the application always leave from startDestination
navigationBackStack.last() != appStartDestinationId -> {
navigationBackStack.removeLast()
navigationBackStack.add(0, appStartDestinationId)
onNavigationItemSelected()
}
// Navigation stack is empty, so finish the activity
else -> activity.finish()
}
}
private class BackStack : ArrayList<Int>() {
companion object {
fun of(vararg elements: Int): BackStack {
val b = BackStack()
b.addAll(elements.toTypedArray())
return b
}
}
fun removeLast() = removeAt(size - 1)
fun moveLast(item: Int) {
remove(item)
add(item)
}
}
}
// Convenience extension to set up the navigation
fun BottomNavigationView.setUpNavigation(bottomNavController: BottomNavController, onReselect: ((menuItem: MenuItem) -> Unit)? = null) {
setOnNavigationItemSelectedListener {
bottomNavController.onNavigationItemSelected(it.itemId)
}
setOnNavigationItemReselectedListener {
bottomNavController.onNavigationItemReselected(it)
onReselect?.invoke(it)
}
bottomNavController.setOnItemNavigationChanged { itemId ->
menu.findItem(itemId).isChecked = true
}
}
Faites votre mise en page main.xml
comme ceci:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:layout_width="match_parent"
Android:layout_height="match_parent">
<FrameLayout
Android:id="@+id/container"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
app:layout_constraintTop_toTopOf="parent" />
<com.google.Android.material.bottomnavigation.BottomNavigationView
Android:id="@+id/bottomNavigationView"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Utilisez sur votre activité comme ceci:
class MainActivity : AppCompatActivity(),
BottomNavController.NavGraphProvider {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.container)
}
private val bottomNavController by lazy(LazyThreadSafetyMode.NONE) {
BottomNavController(this, R.id.container, R.id.navigation_feed)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
bottomNavController.setNavGraphProvider(this)
bottomNavigationView.setUpNavigation(bottomNavController)
if (savedInstanceState == null) bottomNavController
.onNavigationItemSelected()
// do your things...
}
override fun getNavGraphId(itemId: Int) = when (itemId) {
R.id.navigation_feed -> R.navigation.navigation_feed
R.id.navigation_explore -> R.navigation.navigation_explore
R.id.navigation_profile -> R.navigation.navigation_profile
else -> R.navigation.navigation_feed
}
override fun onSupportNavigateUp(): Boolean = navController
.navigateUp()
override fun onBackPressed() = bottomNavController.onBackPressed()
}