IdAndro
IdAndro

Reputation: 1483

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

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?

Upvotes: 146

Views: 52073

Answers (12)

blackorbs
blackorbs

Reputation: 625

Update:

Android Navigation Component now support back stack state handling by default, just add the latest version of the dependencies to your project, check latest release.

implementation("androidx.navigation:navigation-fragment:2.7.7")
implementation("androidx.navigation:navigation-ui:2.7.7")

However, even though your fragment is not destroyed when you navigate back to it, the fragment views are destroyed and onDestroyView() is called when you navigate to other fragment so you should save your views in fragment parameters and perform necessary check to avoid reinitialising or setting listeners again when fragment recreate.

This is how I set my fragment's onCreateView() with binding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if(binding==null) {
            binding = FragmentHomeBinding.inflate(inflater);
            // initialize your variables and set listeners
        }
        return binding.getRoot();
    }

Also check that you're not destroying your views in onDestroyView(), if you need to release memory, do it in onDestroy().

Upvotes: 1

zoulou
zoulou

Reputation: 121

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

Upvotes: 0

Samuel Robert
Samuel Robert

Reputation: 11062

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.

Upvotes: 1

rafsanahmad007
rafsanahmad007

Reputation: 23881

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

Upvotes: 1

KFJK
KFJK

Reputation: 135

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

Upvotes: 1

Zakhar Rodionov
Zakhar Rodionov

Reputation: 1493

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.

Upvotes: 36

Sai
Sai

Reputation: 15738

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
}

Upvotes: 2

Victorlopesjg
Victorlopesjg

Reputation: 79

If you have trouble passing arguments add:

fragment.arguments = args

in class KeepStateNavigator

Upvotes: 2

STAR_ZERO
STAR_ZERO

Reputation: 1460

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.

Upvotes: 83

akhris
akhris

Reputation: 601

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.

Upvotes: 0

veeyikpong
veeyikpong

Reputation: 867

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.

Upvotes: 1

Piotr Prus
Piotr Prus

Reputation: 355

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

Upvotes: 17

Related Questions