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

11

143

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?

Oleaceous answered 23/5, 2018 at 10:28 Comment(1)
Per comment on issuetracker.google.com/issues/80029773 it appears this will be resolved eventually. I'd also be curious however if folks have a cheap workaround that doesn't involve abandoning the library to make this work in the interim.Hills
B
83

Try this.

Navigator

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)
    }
}

NavHostFragment

Create custom NavHostFragment.

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

navigation.xml

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.

Blondell answered 4/8, 2018 at 9:9 Comment(24)
How can I do push to secondView by Code? How to call navigate method?Tait
While this solution works (fragments are reused, not re-created), there's a problem with the navigation. Each menuitem in bottomnavigationview is considered primary - thus, when BACK is pressed, the app finishes (or goes to the top component). A better solution would be to behave just like the YouTube app: (1) Tap Home; (2) Tap Trending; (3) Tap Inbox; (4) Tap Trending; (5) Tap BACK - goes to Inbox; (6) Tap BACK - goes to Home; (7) Tap BACK - app exists. In summary, YouTube BACK functionality between "menuitems" goes back to the most recent, without repeating items.Monobasic
Additionally if you want to use both fragment and custom_fragment in same navigation xml file, you should add super.createFragmentNavigator() to navController in overridden createFragmentNavigator() by passing it to navController.navigatorProvider.addNavigator. This way, items starting with <fragment tag will continue to behave normally while items with type <custom_fragment will end up in your navigator.Commercial
How to keep back button functionality? App is finishing when back button is pressed.Shoemaker
You could use my library to solve this issue: github.com/ZachBublil/ZNavigatorAniseikonia
It's throwing a weird java.lang.IllegalStateException: Could not find Navigator with name "keep_state_fragment" even though I added the navigator using navigatorProvider.Clichy
@jL4, I had the same issue, probably you forgot to remove app:navGraph from your activity's NavHostFragment.Pillowcase
I'm facing the same issue as @Francis. The app exits when the back button is pressed even though app:defaultNavHost="true" is enabledReina
Deep link not working properly if you want to open detail page. Did you try deep linking? I tried change start destination as navigation_home and now deep link working correctlyMontagnard
Why google doesn't sheep this functionality out of the box?Barfield
Btw what if replace attach/detach methods to show/hide? It will keep reusable fragments with all UI changes like EditText typed in text and RecyclerView scroll position basically everything. Because with current solution all UI changes just will wipe out when once fragment will switch.Barfield
NavigationComponent should become a solution not become another problem so programmer must create workaround like this.Anthropogeography
@Blondell thanks for your answer, it was really useful for me, do you know whay savedInstanceState is always null and how I can fix it? How I can identify that fragment was recreated?Explication
Do you have any java version? I tried to convert your code to java, and all fragments overlay each otherMalamut
@Blondell This is wonderful exactly what I was looking for. I wanted to know how did you figured it out on how to use a custom Fragment Navigator and link it with the NavController. Am I just bad at reading documentation or did you find any docs that mention it how to do it ? Because I couldn't find it in official docs.Oringa
@SyedAhmedJamil Here is official docs developer.android.com/guide/navigation/navigation-add-new However, the docs is not included how to implement CustomNavigator. To implement this, I did trial and error, read implementation of Navigation Component.Blondell
@Blondell by the way, has the FragmentManager internally changed after you posted your answer ? Because I couldn't find dispatchOnNavigatorNavigated() method anywhere in the fragment navigator class. Although I was able to implement it even without that method but I'm just curious. Can you take a look inside FragmentNavigator and let me know if I'm right or wrong ?Oringa
@SyedAhmedJamil My answer is old. So please check this repository github.com/STAR-ZERO/navigation-keep-fragment-sampleBlondell
@Blondell do we necessarily need databinding changes to make this work? Or it could work without having to wrap all our layout with the <layout> tags?Princely
@Barfield Because the best they can do is to create shitty components talk about it in every conference, make it look like a solution when in reality it's just another flex tape to fix the root design flaws in AndroidClackmannan
Is there a chance to get this in Java?Ketchan
And what happens if two fragments of the navigation depend of each other?Mumps
your sample is working fine, it is not recreating fragment. but when I followed your code, then only destination fragment is not recreating, except it all fragment is being recreated, can you tell me, what can be wrong in my code.Creep
It works only for graph start destination fragment. Also please actualize the approach using the latest NavComponent (some methods were deprecated)Hastings
F
37

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.

Fossette answered 22/3, 2019 at 11:54 Comment(9)
But it works only for bottom navigation. If you have a general navigation, you have a problemEnough
I'm facing the same issue and every solution I check it contains of kotlin code but I'm afraid that I use Java in my project. Can someone help me how to fix this issue in java.Kartis
@CodeREDInnovations me too, although i'd tried and sucesfull compiled with the kotlin file the navigation stays like this: imgur.com/a/DcsKqPr it doesn't "replace", beside that, it seems the app become heavier and stucking.Xanthe
@AcauãPitta did you find a solution in Java?Ketchan
@CodeREDInnovations We can use extension functions with kotlin in java file. To do this, you need to configure kotlin in gradle, it looks like this is the most optimal way.Fossette
when copying the file, make sure to create a separated nav graph for each bottomNav selection and make sure that the id of the menu item matches with the graph idEirena
How to add the multiple backstack implementation to our code please? I've been trying that since unsuccessfully #68043091Krebs
I want to change to 2.4.0. Any ideas? I used the old sample beforeGroundsill
Nav version 2.5.3: the only main graph startDestination target fragment is keeping in memory, other fragments are still have been recreated. Useless fix, Google is not ok here too.Hastings
M
17

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)
    }
Manes answered 29/3, 2019 at 14:55 Comment(2)
How and where to use this? If I navigate from Fragment A to B, then From B to A how could I go without calling oncraeteView() and onViewCreated() methods, in short without restarting fragment AQuinones
@UsamaSaeedUS: Can you please share code, how to use it? Like what is mentioned by Kishan.Melissamelisse
H
2

If you have trouble passing arguments add:

fragment.arguments = args

in class KeepStateNavigator

Horse answered 17/10, 2019 at 16:7 Comment(0)
K
2

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
}
Kidder answered 4/4, 2021 at 11:47 Comment(3)
This is not correct unless the Parcelable in the viewmodel is actually set on a SavedStateHandleGadhelic
@Gadhelic if you just want to retain scroll position while the process is still running (e.g. user switching between different tabs in the same "session") it should also work without a SavedStateHandle.Coupler
Tried it, not always work.Cavazos
L
1

Not available as of now.

As a workaround you could store all your fetched data into ViewModel and have that data readily available when you recreate the fragment. Make sure you get the ViewModel object using activity context.

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

Loader answered 20/9, 2018 at 13:9 Comment(2)
This is indeed true, and data > view but the problem is when you want to also keep the view state, e.g. multiple pages loaded and the user has scrolled down :).Stubblefield
@JoaquimLey View states are never meant to be stored if the user navigates out of an activity or fragment like in this case. However, It could be useful when there is a process death due to system constraints, in such case you can store in the savedInstanceState bundle.Loader
P
1

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.

Pol answered 27/4, 2019 at 6:4 Comment(1)
idk why but currentDestination!!.id always giving me same values for all 3 fragmentsAromaticity
R
1

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

Resolvable answered 24/9, 2021 at 0:42 Comment(4)
Hello. "it will remember your last fragment from the stack"--What adapter?Groundsill
@Groundsill the Adapter code I wrote is your custom adapter - where you can navigate to the second fragment. The back stack is automatically managed by Android navigation component NavController Class. developer.android.com/guide/navigation/…Resolvable
Your answer is how to go to the Fragment which is visited most recently when press Back, but will not restore the previous work of the Fragment. Question is related to restore previous work of the fragmentSkippet
And what If I am not using the "bottom navigation view"?Fuchsia
H
1

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).

Harumscarum answered 14/10, 2021 at 7:12 Comment(0)
M
0

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.

Multimillionaire answered 29/7, 2019 at 5:55 Comment(1)
I'm glad my solution worked for you. I'm actually checking current menuItem in bottomNavigationView before calling fragment from backStack using: bottomNavigationView.setOnNavigationItemSelectedListenerManes
A
0

Update 2023-06-10:

Below is no longer working since androidx.navigation:navigation-fragment-ktx:2.6.0

Original post

I tried STAR_ZERO's solution https://github.com/STAR-ZERO/navigation-keep-fragment-sample for several hours and found it not working in my app first of all.

Finally succeeded to achieve what I wanted: My main Fragment in main Activity should not be re-created each time when navigating away and then back using NavigationBarView or BottomNavigationView.

Limitations:

  1. Works only exactly with the nav_graph's startDestination (here: app:startDestination="@id/nav_home"), Note: All other fragments require the <keep_state_fragment ...> too!
  2. Using setOnItemSelectedListener (NavigationBarView.OnItemSelectedListener listener) seems to conflict with the intention to not re-create a fragment

Using these versions:

dependencies {
   implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
   implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
}

layout/activity_main.xml

...
<!-- Do NOT add app:navGraph="@navigation/nav_graph" -->
<androidx.fragment.app.FragmentContainerView 
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:defaultNavHost="true"        
...

navigation/nav_graph.xml

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

<!-- ALL fragments must be keep_state_fragment,
     otherwise nav_home will not behave as keep_state_fragment -->
    <keep_state_fragment
        android:id="@+id/nav_home"
        ... />
    <keep_state_fragment
        android:id="@+id/nav_other"
        ... />

KeepStateNavigator.kt (yes ... nearly empty class)

@Navigator.Name("keep_state_fragment") // 'keep_state_fragment' is used in navigation/nav_graph.xml
class KeepStateNavigator(
    private val context: Context,
    private val manager: FragmentManager, // MUST pass childFragmentManager.
    private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
    
    /* NOTE: override fun navigate(...) is never called, so not needed */
}  

MainActivity.java

...
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());

    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    Fragment navHostFragment = getSupportFragmentManager()
                .findFragmentById(R.id.nav_host_fragment);
    FragmentManager childFragmentManager = navHostFragment.getChildFragmentManager();
    // Add our KeepStateNavigator to NavController's NavigatorProviders
    navController.getNavigatorProvider().addNavigator(
                    new KeepStateNavigator(this, childFragmentManager,
                            R.id.nav_host_fragment));

    // must be here, not in layout/activity_main.xml,
    // because we create KeepStateNavigator after NavigationBarView was inflated
    navController.setGraph(R.navigation.nav_graph);

    // Do NOT set a NavigationBarView.OnItemSelectedListener
    // Seems to conflict with the intention to not re-create Fragment
    // DO NOT: *NavigationBarView*.setOnItemSelectedListener(...);
    
    // Done.
    NavigationUI.setupWithNavController(navBarView, navController);
}
Anesthesiology answered 4/6, 2023 at 17:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.