web-dev-qa-db-fra.com

Android Jetpack Navigation, BottomNavigationView avec Youtube ou Instagram comme une navigation arrière correcte (fragment de pile arrière)?

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

34
Bincy Baby

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:

 enter image description here

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.

26
Levi Moreira

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 .  enter image description here

14
SANAT

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

7
Suhaib Roomy

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();
}
4
Mohammad Ersan

Si vous avez une bottomNavigationView avec 3 éléments correspondant à 3 Fragments: FragmentA, FragmentB et FragmentCFragmentA 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.

2
Husayn Hakeem

Tout d’abord, laissez-moi clarifier la manière dont Youtube et Instagram gèrent la navigation par fragments.

  • Lorsque l'utilisateur se trouve sur un fragment de détail, effectuez une sauvegarde en amont ou en aval de la pile une fois, avec l'état restauré correctement. Un deuxième clic sur l’élément de barre inférieur déjà sélectionné fait apparaître toute la pile à la racine, ce qui la rafraîchit.
  • Lorsque l'utilisateur est sur un fragment racine, retour au dernier menu sélectionné dans la barre inférieure, affichant le dernier fragment de détail, avec l'état correctement restauré (JetPack non)
  • Lorsque l'utilisateur est sur le fragment de destination de départ, l'activité se termine

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()
}
0
Allan Veloso