Reputation: 1483
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
Reputation: 625
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
Reputation: 121
Below is no longer working since androidx.navigation:navigation-fragment-ktx:2.6.0
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:
app:startDestination="@id/nav_home"
), Note: All other fragments require the <keep_state_fragment ...>
too!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
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
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
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
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
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
Reputation: 79
If you have trouble passing arguments add:
fragment.arguments = args
in class KeepStateNavigator
Upvotes: 2
Reputation: 1460
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>
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>
I created sample project. link
I don't create custom NavHostFragment. I use navController.navigatorProvider += navigator
.
Upvotes: 83
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
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
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