Reputation: 71
I've set up bottom navigation with nav graph
, in the most basic way -
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.navController)
the fragment that's declared as startDestination
is never destroyed when navigating from it (only paused) while all other fragments are destroyed when navigating away.
(I need it to be destroyed so that in the viewModel associated with it onCleared()
will be called).
Any idea why? or How to change this behavior?
navigation:
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation"
app:startDestination="@id/drawingFragment">
<fragment
android:id="@+id/controllerFragment"
android:name="com.example.android.myApp.ControllerFragment"
android:label="fragment_controller"
tools:layout="@layout/fragment_controller" >
<action
android:id="@+id/action_controllerFragment_to_drawingFragment"
app:destination="@id/drawingFragment" />
</fragment>
<fragment
android:id="@+id/drawingFragment"
android:name="com.example.android.myApp.DrawingFragment"
android:label="fragment_drawing"
tools:layout="@layout/fragment_drawing" >
<action
android:id="@+id/action_drawingFragment_to_clippingFragment"
app:destination="@id/clippingFragment"
app:launchSingleTop="true"
app:popUpTo="@+id/drawingFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/clippingFragment"
android:name="com.example.android.myApp.ClippingFragment"
android:label="fragment_clipping"
tools:layout="@layout/fragment_clipping" />
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var navHostFragment: NavHostFragment
private lateinit var bottomNavigationView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setUpNavigation()
}
fun setUpNavigation(){
bottomNavigationView = findViewById(R.id.bttm_nav)
navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.navController)}
activity_main/xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bttm_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemTextAppearanceActive="@style/bottomNaActive"
app:itemTextAppearanceInactive="@style/bottomNavInactive"
app:layout_constraintBottom_toBottomOf="@+id/nav_host_fragment"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_menu_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
Upvotes: 6
Views: 2808
Reputation: 980
@chrgue described the problem correctly
In case of "top level" navigation, there is no concept of is no up
or down
. But startDestination
fragment is kept in memory, and, at the same time it is recreated when switching to it.
What is especially unpleasant if the application only contains "top level" fragments.
I do not know how to properly solve this problem. For myself, I writed the next code.
The code turned out to be complicated because I had to solve problems: the back button did not work correctly and did not recover after OOM (also action navigation with "pop to"), and it is a terrible hack
extension method
fun FragmentActivity.enableDestroyStartDestination(
navController: NavController,
appBarConfiguration: AppBarConfiguration
) {
val startDestinationId = navController.graph.startDestination
var firstStart = true
var preventMainRecursionFlag = false
var latestDestionationIsMain = false
navController.addOnDestinationChangedListener { controller, destination, args ->
if (appBarConfiguration.topLevelDestinations.contains(destination.id)) {
if (destination.id == startDestinationId) {
latestDestionationIsMain = true
if (firstStart) {
firstStart = false
return@addOnDestinationChangedListener
}
if (preventMainRecursionFlag) {
preventMainRecursionFlag = false
return@addOnDestinationChangedListener
}
preventMainRecursionFlag = true
val options = NavOptions.Builder().setLaunchSingleTop(true).build()
controller.navigate(startDestinationId, args, options)
} else {
val navHostFragment =
supportFragmentManager.primaryNavigationFragment as NavHostFragment
if (navHostFragment.childFragmentManager.fragments.size > 0) {
if (latestDestionationIsMain) {
val fragment = navHostFragment.childFragmentManager.fragments[0]
navHostFragment.childFragmentManager.beginTransaction().remove(fragment)
.commitNowAllowingStateLoss()
}
}
latestDestionationIsMain = false
}
} else {
latestDestionationIsMain = false
}
}
}
usage (Navigation Drawer or BottomNavigationView)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
appBarConfiguration = AppBarConfiguration(
setOf(... ), drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
this.enableDestroyStartDestination(navController, appBarConfiguration)
}
I can't guarantee it will work anywhere - my application is too simple.
Upvotes: 0
Reputation: 639
this is actually no answer rather than a comment on @isrzanza's answer (sorry, not enough reputation).
I assumed in case of BottomNavigationView there is no up or down. For me each fragment which I can navigate from here a more like neighbors respectively equal important so I thought not only the ViewModels of these were cleared rather I asssumed that the fragment itself would be destroyed via onDestroy when navigating away.
I don't know why specially the start fragment should stay in memory and the others don't, because they are equal important, didn't they?
EDIT:
I also want to mention I noticed if I navigate to the startDestination-fragment again a new fragment of same type is created (onCreate will be executed again) and the old one will be destroyed (onDestroy will be executed). For me this is a other waste in resources. To keep the fragment for this situation in memory and re-create it afterwards anyway makes no sense to me. Hope I misunderstood something here :)
Upvotes: 2
Reputation: 34
What you are describing is the default behavior of the navigation component. when navigating down, the fragment you navigated from is not destroyed, only when navigating up.
personally I don't understand why you would want to notify the viewModel the fragment is destroyed, but if you want to run a certain piece of code when you are navigating to another destination, you could use NavController.OnDestinationChangedListener in your main activity (or in your fragment, but don't forget to remove the listener when it is destroyed), and do some action according to your start and end destinations.
if you want to destroy the fragment anyway you can try changing the "pop to" parameter in the navigation action in your nav graph.
Upvotes: 0