Is there a way to keep fragment alive when using BottomNavigationView with new NavController?

AndroidNavigationBottomnavigationview

Android Problem Overview


I'm trying to use the new navigation component. I use a BottomNavigationView with the navController : NavigationUI.setupWithNavController(bottomNavigation, navController)

But when I'm switching fragments, they are each time destroy/create even if they were previously used.

Is there a way to keep alive our main fragments link to our BottomNavigationView?

Android Solutions


Solution 1 - Android

Try this.

Create custom navigator.

@Navigator.Name("custom_fragment")  // Use as custom tag at navigation.xml
class CustomNavigator(
    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?) {
        val tag = destination.id.toString()
        val transaction = manager.beginTransaction()

        val currentFragment = manager.primaryNavigationFragment
        if (currentFragment != null) {
            transaction.detach(currentFragment)
        }

        var fragment = manager.findFragmentByTag(tag)
        if (fragment == null) {
            fragment = destination.createFragment(args)
            transaction.add(containerId, fragment, tag)
        } else {
            transaction.attach(fragment)
        }

        transaction.setPrimaryNavigationFragment(fragment)
        transaction.setReorderingAllowed(true)
        transaction.commit()

        dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
    }
}

Create custom NavHostFragment.

class CustomNavHostFragment: NavHostFragment() {
    override fun onCreateNavController(navController: NavController) {
        super.onCreateNavController(navController)
        navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
    }
}

Use custom tag instead of fragment tag.

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation"
    app:startDestination="@id/navigation_first">

    <custom_fragment
        android:id="@+id/navigation_first"
        android:name="com.example.sample.FirstFragment"
        android:label="FirstFragment" />
    <custom_fragment
        android:id="@+id/navigation_second"
        android:name="com.example.sample.SecondFragment"
        android:label="SecondFragment" />
</navigation>
activity layout

Use CustomNavHostFragment instead of NavHostFragment.

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="com.example.sample.CustomNavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/navigation" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

Update

I created sample project. link

I don't create custom NavHostFragment. I use navController.navigatorProvider += navigator.

Solution 2 - Android

Update 19.05.2021 Multiple backstack
Since Jetpack Navigation 2.4.0-alpha01 we have it out of the box. Check Google Navigation Adavanced Sample

Old answer:
Google samples link Just copy NavigationExtensions to your application and configure by example. Works great.

Solution 3 - Android

After many hours of research I found solution. It was all the time right in front of us :) There is a function: popBackStack(destination, inclusive) which navigate to given destination if found in backStack. It returns Boolean, so we can navigate there manually if the controller won't find the fragment.

if(findNavController().popBackStack(R.id.settingsFragment, false)) {
        Log.d(TAG, "SettingsFragment found in backStack")
    } else {
        Log.d(TAG, "SettingsFragment not found in backStack, navigate manually")
        findNavController().navigate(R.id.settingsFragment)
    }

Solution 4 - Android

If you have trouble passing arguments add:

fragment.arguments = args

in class KeepStateNavigator

Solution 5 - Android

Not available as of now.

As a workaround you could store all your fetched data into view model and have that data readly available when you recreate the fragment. Make sure you get the view using activities context.

You can use LiveData to make your data lifecycle-aware observable

Solution 6 - Android

If you are here just to maintain the exact RecyclerView scroll state while navigating between fragments using BottomNavigationView and NavController, then there is a simple approach that is to store the layoutManager state in onDestroyView and restore it on onCreateView

I used ActivityViewModel to store the state. If you are using a different approach make sure you store the state in the parent activity or anything which survives longer than the fragment itself.

Fragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    recyclerview.adapter = MyAdapter()
    activityViewModel.listStateParcel?.let { parcelable ->
        recyclerview.layoutManager?.onRestoreInstanceState(parcelable)
        activityViewModel.listStateParcel = null
    }
}

override fun onDestroyView() {
    val listState = planet_list?.layoutManager?.onSaveInstanceState()
    listState?.let { activityViewModel.saveListState(it) }
    super.onDestroyView()
}

ViewModel

var plantListStateParcel: Parcelable? = null

fun savePlanetListState(parcel: Parcelable) {
    plantListStateParcel = parcel
}

Solution 7 - Android

I've used the link provided by @STAR_ZERO and it works fine. For those who having problem with the back button, you can handle it in the activity / nav host like this.

override fun onBackPressed() {
        if(navController.currentDestination!!.id!=R.id.homeFragment){
            navController.navigate(R.id.homeFragment)
        }else{
            super.onBackPressed()
        }
    }

Just check whether current destination is your root / home fragment (normally the first one in bottom navigation view), if not, just navigate back to the fragment, if yes, only exit the app or do whatever you want.

Btw, this solution need to work together with the solution link above provided by STAR_ZERO, using keep_state_fragment.

Solution 8 - Android

The solution provided by @piotr-prus helped me, but I had to add some current destination check:

if (navController.currentDestination?.id == resId) {
    return       //do not navigate
}

without this check current destination is going to recreate if you mistakenly navigate to it, because it wouldn't be found in back stack.

Solution 9 - Android

In the latest Navigation component release - bottom navigation view will keep track of the latest fragment in stack.

Here is a sample:

https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample

Example code
In project build.gradle

dependencies {  
      classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha01"
}

In app build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'androidx.navigation.safeargs'
}

dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.4.0-alpha01"

}

Inside your activity - you can setup navigation with toolbar & bottom navigation view

val navHostFragment = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
 //setup with bottom navigation view
binding.bottomNavigationView.setupWithNavController(navController)
//if you want to disable back icon in first page of the bottom navigation view
val appBarConfiguration = AppBarConfiguration(
	setOf(
                R.id.feedFragment,
                R.id.favoriteFragment
            )
        ).
//setup with toolbar back navigation
binding.toolbar.setupWithNavController(navController, appBarConfiguration)

Now in your fragment, you can navigate to your second frgment & when you deselect/select the bottom navigation item - NavController will remember your last fragment from the stack.

Example: In your Custom adapter

adapter.setOnItemClickListener { item ->
            findNavController().navigate(
                R.id.action_Fragment1_to_Fragment2
       )
}

Now, when you press back inside fragment 2, NavController will pop fragment 1 automatically.

https://developer.android.com/guide/navigation/navigation-navigate

Solution 10 - Android

Super easy solution for custom general fragment navigation:

Step 1

Create a subclass of FragmentNavigator, overwrite instantiateFragment or navigate as you need. If we want fragment only create once, we can cache it here and return cached one at instantiateFragment method.

Step 2

Create a subclass of NavHostFragment, overwrite createFragmentNavigator or onCreateNavController, so that can inject our customed navigator(in step1).

Step 3

Replace layout xml FragmentContainerView tag attribute from android:name="com.example.learn1.navigation.TabNavHostFragment" to your customed navHostFragment(in step2).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionIdAndroView Question on Stackoverflow
Solution 1 - AndroidSTAR_ZEROView Answer on Stackoverflow
Solution 2 - AndroidZakhar RodionovView Answer on Stackoverflow
Solution 3 - AndroidPiotr PrusView Answer on Stackoverflow
Solution 4 - AndroidVictorlopesjgView Answer on Stackoverflow
Solution 5 - AndroidSamuel RobertView Answer on Stackoverflow
Solution 6 - AndroidSaiView Answer on Stackoverflow
Solution 7 - AndroidveeyikpongView Answer on Stackoverflow
Solution 8 - AndroidakhrisView Answer on Stackoverflow
Solution 9 - Androidrafsanahmad007View Answer on Stackoverflow
Solution 10 - AndroidKFJKView Answer on Stackoverflow