Publicado en Android

Mantener estado de los fragmentos en BottomNavigationView (Jetpack)


Ejemplo de implementación de un BottomNavigationView con el sistema de navegación del componente navigation del cojunto de componentes para Android (Jetpack) y que los fragmentos mostrados mantengan su estado

Partiendo de generar la plantilla con AndroidStudio seleccionar el BottomNavigation:

Dependencias

En el app.gradle añadir las siguientes dependencias

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'androidx.navigation:navigation-fragment:2.2.1'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
    implementation 'androidx.navigation:navigation-ui:2.2.1'
    implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'

    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Layout principal

En el fragment nav_host_fragment se debe remover la asignación de la navegación, porque se implementa des de código

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="?attr/actionBarTheme">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="?attr/actionBarPopupTheme">

        </androidx.appcompat.widget.Toolbar>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="?attr/actionBarSize"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottomNavView"
            style="@style/Widget.MaterialComponents.BottomNavigationView.PrimarySurface"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"

            app:menu="@menu/bottom_nav_menu" />

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/nav_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Navegación

Los cambios es que antes de declarar con fragment se hace con keep_state_fragment

<?xml version="1.0" encoding="utf-8"?>
<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/mobile_navigation"
    app:startDestination="@+id/navigation_dashboard">

    <keep_state_fragment
        android:id="@+id/navigation_home"
        android:name="app.webserveis.testbottomnavigation.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <keep_state_fragment
        android:id="@+id/navigation_dashboard"
        android:name="app.webserveis.testbottomnavigation.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <keep_state_fragment
        android:id="@+id/navigation_notifications"
        android:name="app.webserveis.testbottomnavigation.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

KeepStateNavigation

Método para mantener los estados de los fragmentos, comprueba en la pila del gestor si no existe el fragmento lo añade add si existe lo recupera quitando el actual con detach y attach con lo que se quiere mostrar

@Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml
class KeepStateNavigator(
    private val context: Context,
    private val manager: FragmentManager,
    private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {

    override fun navigate(
        destination: Destination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ): NavDestination? {
        val tag = destination.id.toString()
        val transaction : FragmentTransaction = manager.beginTransaction()

        val currentFragment: Fragment? = manager.primaryNavigationFragment

        var initialNavigate = false
        if (currentFragment != null) {
            transaction.detach(currentFragment)
        } else {
            initialNavigate = true
        }

        var fragment: Fragment? = manager.findFragmentByTag(tag)

        if (fragment == null) {
            fragment =
                manager.fragmentFactory.instantiate(context.classLoader, destination.className)
            transaction.add(containerId, fragment, tag)
        } else {
            transaction.attach(fragment)
        }

        transaction.apply {
            setPrimaryNavigationFragment(fragment)
            setReorderingAllowed(true)
        }.commitNow()

        return if (initialNavigate) {
            destination
        } else {
            null
        }
    }
}

Implementación

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.title = getText(R.string.app_name)
        setupNavigationKeepState()
    }

    fun setupNavigationKeepState() {
        val navController = findNavController(R.id.nav_host_fragment)

        val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!

        val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
        navController.navigatorProvider += navigator

        navController.setGraph(R.navigation.nav_graph)

        bottomNavView.setupWithNavController(navController)
    }

    companion object {
        val TAG: String = MainActivity::class.java.simpleName
    }
}

Fragmentos

A cada fragmento que debemos asignar el titulo en onCreatedView

requireActivity().toolbar.title = "Home"

Enlaces

Solución modificada de Star-Zero Keep Navigation Kepp fragment Sample