141

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?

Rahul
  • 3,293
  • 2
  • 31
  • 43
IdAndro
  • 1,433
  • 2
  • 8
  • 7
  • 7
    Per comment on https://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. – Jimmy Alexander Jun 15 '18 at 18:51

11 Answers11

82

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.

Link182
  • 733
  • 6
  • 15
STAR_ZERO
  • 1,400
  • 11
  • 21
  • How can I do push to secondView by Code? How to call navigate method? – Patrick Oct 03 '18 at 13:46
  • 3
    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. – miguelt Oct 10 '18 at 19:10
  • 1
    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 ``` – Mel Oct 11 '18 at 11:44
  • 3
    How to keep back button functionality? App is finishing when back button is pressed. – Francis Nov 23 '18 at 18:01
  • You could use my library to solve this issue: https://github.com/ZachBublil/ZNavigator – Zach Bublil Dec 03 '18 at 14:49
  • 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`. – jL4 Dec 06 '18 at 11:09
  • 6
    @jL4, I had the same issue, probably you forgot to remove `app:navGraph` from your activity's NavHostFragment. – Paul Chernenko Dec 14 '18 at 14:25
  • 2
    I'm facing the same issue as @Francis. The app exits when the back button is pressed even though `app:defaultNavHost="true"` is enabled – Raicky Derwent Dec 17 '18 at 11:14
  • 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 correctly – toffor Jan 28 '19 at 08:01
  • 10
    Why google doesn't sheep this functionality out of the box? – Arsenius Feb 22 '19 at 04:35
  • 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. – Arsenius Feb 22 '19 at 11:42
  • 108
    NavigationComponent should become a solution not become another problem so programmer must create workaround like this. – Arie Agung Mar 27 '19 at 16:22
  • @STAR_ZERO 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? – Viktor Apoyan Apr 23 '19 at 07:16
  • 1
    Do you have any java version? I tried to convert your code to java, and all fragments overlay each other – user2905416 Oct 22 '19 at 07:04
  • 1
    @STAR_ZERO 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. – Syed Ahmed Jamil Dec 12 '19 at 00:50
  • @SyedAhmedJamil Here is official docs https://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. – STAR_ZERO Dec 13 '19 at 04:58
  • 2
    @STAR_ZERO 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 ? – Syed Ahmed Jamil Dec 17 '19 at 01:27
  • 4
    @SyedAhmedJamil My answer is old. So please check this repository https://github.com/STAR-ZERO/navigation-keep-fragment-sample – STAR_ZERO Dec 18 '19 at 11:45
  • 1
    @STAR_ZERO do we necessarily need `databinding` changes to make this work? Or it could work without having to wrap all our layout with the `` tags? – faizanjehangir Feb 06 '20 at 18:04
  • 2
    @Arsenius 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 Android – Farid Jul 14 '20 at 19:04
  • 1
    Is there a chance to get this in Java? – developKinberg Nov 12 '20 at 15:46
  • And what happens if two fragments of the navigation depend of each other? – Laura Galera Jun 03 '21 at 16:25
  • 1
    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. – Shubham Mogarkar Jun 21 '22 at 11:51
  • It works only for graph start destination fragment. Also please actualize the approach using the latest NavComponent (some methods were deprecated) – Konstantin Konopko Apr 07 '23 at 12:19
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.

Zakhar Rodionov
  • 1,398
  • 16
  • 18
  • 6
    But it works only for bottom navigation. If you have a general navigation, you have a problem – Georgiy Chebotarev Jun 23 '20 at 20:13
  • 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. – CodeRED Innovations Jul 09 '20 at 11:56
  • @CodeREDInnovations me too, although i'd tried and sucesfull compiled with the kotlin file the navigation stays like this: https://imgur.com/a/DcsKqPr it doesn't "replace", beside that, it seems the app become heavier and stucking. – Acauã Pitta Oct 06 '20 at 05:35
  • @AcauãPitta did you find a solution in Java? – developKinberg Nov 12 '20 at 15:48
  • @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. – Zakhar Rodionov Nov 19 '20 at 15:46
  • 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 id – Aleyam Feb 14 '21 at 16:24
  • How to add the multiple backstack implementation to our code please? I've been trying that since unsuccessfully https://stackoverflow.com/questions/68042591/problem-upgrading-my-fragments-navigation-versionfrom-2-3-5-to-2-4-0-alpha03 – Richard Wilson Jun 23 '21 at 19:17
  • I want to change to 2.4.0. Any ideas? [I used the old sample before](https://stackoverflow.com/questions/70858513/multi-navgraph-bottomnavigationview-in-navigation-2-4-0) – Krahmal Jan 26 '22 at 05:21
  • 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. – Konstantin Konopko Apr 06 '23 at 20:18
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)
    }
Piotr Prus
  • 334
  • 3
  • 10
  • 8
    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 A – Kishan Solanki May 06 '20 at 16:10
  • 1
    @UsamaSaeedUS: Can you please share code, how to use it? Like what is mentioned by Kishan. – Code_Life Jun 21 '20 at 06:09
2

If you have trouble passing arguments add:

fragment.arguments = args

in class KeepStateNavigator

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
}
Sai
  • 15,188
  • 20
  • 81
  • 121
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.

Samuel Robert
  • 10,106
  • 7
  • 39
  • 60
  • 3
    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 :). – Joaquim Ley Jul 14 '19 at 18:37
  • @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. – Samuel Robert Jul 07 '22 at 07:43
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.

veeyikpong
  • 829
  • 8
  • 20
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

rafsanahmad007
  • 23,683
  • 6
  • 47
  • 62
  • 1
    Hello. "it will remember your last fragment from the stack"--What adapter? – Krahmal Jan 26 '22 at 05:26
  • @Krahmal 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. https://developer.android.com/guide/navigation/navigation-navigate#back-stack – rafsanahmad007 Jan 28 '22 at 11:24
  • 1
    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 fragment – 4rigener Sep 14 '22 at 21:54
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).

KFJK
  • 115
  • 6
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.

akhris
  • 475
  • 4
  • 7
  • I'm glad my solution worked for you. I'm actually checking current menuItem in bottomNavigationView before calling fragment from backStack using: bottomNavigationView.setOnNavigationItemSelectedListener – Piotr Prus Apr 14 '20 at 07:17
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);
}
zoulou
  • 121
  • 4