Android Jetpack Navigation nested tab backward navigation strange behaviour - android

So I'm trying Jetpack navigation component with BottomNavigationView. I created two layer of BottomNavigationView, and the structure looks like this:
MainActivity (with nav_host_fragment, navigation_graph, bottom_navigation)
FragmentA
FragmentB
FragmentC (with nested_nav_host_fragment, nested_navigation_graph, nested_bottom_navigation)
FragmentCA
FragmentCB
FragmentCC
I have no problem navigating forward, but I couldn't navigate backward properly.
For example, when I navigation from A -> B -> C, and in C navigate CA -> CB -> CC, then clicking back button or calling navControler back, it should go from CC -> CB -> CA -> B -> A, but it straightly went to A instead.
The minimum demo project can be found here, hope someone can help, thanks.

By default, Fragments do not pop anything added to the back stack of child fragments.
To get the system back button to pop child Fragments of your Fragment C, you must specifically opt into that behavior by calling setPrimaryNavigationFragment().
This can be done anywhere in your Fragment after your Fragment is attached. For example, you can update your FragmentC to do it in onActivityCreated():
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
NavigationUI.setupWithNavController(nested_bottom_navigation,
activity?.findNavController(R.id.nested_nav_host_fragment)?:return)
// This routes the system back button to this Fragment
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit()
}
This is actually the same technique that the app:defaultNavHost="true" attribute on NavHostFragment is using under the hood.

Related

Navigate to specific fragments with navigation component

I currently have this nav_graph.xml file.
Activity A (parent container), with this navigation tree:
Fragment A -> Fragment B -> Fragment C -> Fragment D -> Fragment E.
Also, I have an Activity B, and I want to navigate directly to Fragment C from this activity.
Does anyone know how I can realize an elegant solution for this case?
I have thought that I could launch an intent from Activity B -> Activity A passing it as an extra an enum corresponding to the desired fragment and that Activity A would handle the received parameter navigating through an action to the corresponding fragment. How do you see it? I think it can be improved and that's why I'm looking for a second solution. thanks!
Add this (your nav host fragment) in both of your activities with
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_view_pager_host"
android:layout_width="match_parent"
android:layout_height="670dp"
app:layout_constraintBottom_toTopOf="#+id/bottomNav"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/main_nav_graph"
app:defaultNavHost="true"/>
and then in your activity B onCreate() just add
val navController = (supportFragmentManager.findFragmentById(R.id.nav_view_pager_host) as NavHostFragment).navController
navController.navigate(/* To your fragment B*/)
//Handle anything else you wanna ddo

How to prevent BottomSheetDialogFragment from dismissing after a navigation to another fragment?

I am using NavigationComponent on my App.
I have an specific flow where after a click on a button of BottomSheetDialogFragment the app should navigate to another fragment. But when that Fragment is popped I need to navigate back to the previous BottomSheetDialogFragment.
For some reason the BottomSheetDialogFragment is being dismissed automatically.
Frag A : click on a button
Frag A -> Dialog B : click on a button
Frag A -> Dialog B -> Frag C : pop Frag C from the stack
Frag A : Dialog B was automatically dismissed =;/
How can one prevent that dismissing?
Q: Why do I need the BottomSheetDialogFragment not dismissed?
A: I listen to the result of the opened fragment through a LiveData. Due to the dismissing of the BottomSheetDialogFragment it never receives the result.
This is not possible. Dialog destinations implement the FloatingWindow interface which states:
Destinations that implement this interface will automatically be popped off the back stack when you navigate to a new destination.
So it is expected that dialog destinations are automatically popped off the back stack when you navigate to a <fragment> destination. This is not the case when navigating between multiple dialog destinations (those can be stacked on top of one another).
This issue explains a bit more about the limitations here, namely that:
Dialogs are separate windows that always sit above your activity's window. This means that the dialog will continue to intercept the system back button no matter what state the underlying FragmentManager is in or what FragmentTransactions you do.
Operations on the fragment container (i.e., your normal destinations) don't affect dialog fragments. Same if you do FragmentTransactions on a nested FragmentManager.
So once you navigate to your <fragment> destination, the only way for the system back button to actually work is for all floating windows to be popped (otherwise they would intercept the back button before anything else) as those windows are always floating above the content.
This isn't a limitation imposed by the Navigation Component - the same issues apply to any usages of BottomSheetDialogFragment regarding the Fragment back stack and the system back button.
This is not possible as pointed out by #ianhanniballake.
But this can be achieved by making fragment C as a DailogFragment, not a normal Fragment, but this requires some effort to make it behave like a normal fragment.
In this case both B & C are dialogs and therefore they'll share the same back stack. and hence when the back stack is poped up to back from C to B, you'll still see the BottomSheetDialgFragment B showing.
To fix the limited window of C use the below theme:
<style name="DialogTheme" parent="Theme.MyApp">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
</style>
Where Theme.MyApp is your app's theme.
And then apply it to C by overriding getTheme():
class FragmentC : DialogFragment() {
//.....
override fun getTheme(): Int = R.style.DialogTheme
}
Also you need to change C in the navigation graph from a fragment to a dialog:
<dialog
android:id="#+id/fragmentC"
android:name="....">
</dialog>
Preview:
You wouldn't want not to dismiss a dialog because it would stay on top of the next destination.
By "listen to result", if you mean findNavController().currentBackStackEntry.savedStateHandle.getLiveData(MY_KEY)
then you should be able to set your result to previousBackStackEntry as it will give you the destination before your dialog.
Frag A : click on a button
Frag A -> Dialog B : click on a button (automatically popped-off)
Frag A -> Dialog B -> Frag C : pop Frag C from the stack
then
class FragA : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
findNavController().currentBackStackEntry.savedStateHandle?.getLiveData<MyResult>(MY_KEY).observe(viewLifecycleOwner) {
// get your result here
// show Dialog B again if you like ?
}
}
}
and
class FragC : Fragment() {
...
private fun setResultAndFinish(result: MyResult) {
findNavController().apply {
previousBackStackEntry?.savedStateHandle?.set(MY_KEY, result)
popBackStack()
}
}
}

Problem going back from current Fragment back to the replaced Fragment when using NavHostFragment

I have three fragments A, B and C. And I'm using navHostFragment container in MainActivity. So the application goes from A -> B using kotlin extension function findNavController().navigate... and then go from B to C using same function. All works fine till here.
Now in Fragment C, I'm replacing different elements on fragment C using
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.list_container, someFragment)
?.addToBackStack("some_frag_id")
?.commit()
The list_container is replaced with someFragment. After this when I press physical back button Fragment C pops out and my app goes to Fragment B while what I expect it to restore replaced list_container i.e. whatever was there before replacement.
I'm also overiding this in my MainActivity
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count == 0) {
super.onBackPressed()
//additional code
}
else {
supportFragmentManager.popBackStack()
}
}
I'm not sure what is missing here. I have read a lot of solutions on stackoverflow but none worked to my satisfaction. Please guide.
If you are adding a Fragment to a View within a Fragment, you must always use the childFragmentManager - using activity?.supportFragmentManager is always the wrong FragmentManager to use in that case.
Besides fixing cases with restoring state (which would not work when using the wrong FragmentManager), this also ensures that the default behavior for dispatching onBackPressed() down the FragmentManager hierarchy will work out the box - you should not need any logic at all in onBackPressed() to have the pop work correctly.
If you need to intercept the back button in Fragment C, you should follow the providing custom back documentation to register an OnBackPressedDispatcher - you should not override onBackPressed() even in those cases.

FragmentScenario and nested NavHostFragments don't perform navigations as expected in Instrumentation tests

I am writing a single Activity app that uses Android's Navigation Components to help with navigation and Fragment Scenario for instrumentation testing. I have run into a performance discrepancy when using the back button between the actual app navigation behavior and the behavior of a Fragment being tested in isolation during an Instrumentation tests when using fragment scenario.
In my MainActivity I have a main NavHostFragment that takes up the entire screen. I use that nav host fragment to show several screens including some master detail fragments. Each master detail fragment has another NavHostFragment in it to show the different detail fragments for that feature. This setup works great and provides the behavior I desire.
To accomplish the master detail screen I use a ParentFragment that has two FrameLayouts to create the split screen for tablet and for handset I programatically hide one of the FrameLayouts. When the ParentFragment is created, it detects if it is being run on a tablet or handset and then programatically adds a NavHostFragment to the right frame layout on tablet, and on handset hides the right pane adds a NavHostFragment to the left pane. The NavHostFragments also have a different navigation graph set on them depending on if they are being run on tablet or handset (on handset we show fragments as dialogs, on tablet we show them as regular fragments).
private fun setupTabletView() {
viewDataBinding.framelayoutLeftPane.visibility = View.VISIBLE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_tablet)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
if (childFragmentManager.findFragmentByTag(SummaryFragment.TAG) == null) {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_right_pane, fragFactory.instantiate(ClassLoader.getSystemClassLoader(), SummaryFragment::class.java.canonicalName!!), SummaryFragment.TAG)
.commit()
}
}
private fun setupPhoneView() {
viewDataBinding.framelayoutLeftPane.visibility = View.GONE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_phone)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.replace(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
}
When running the devDebug version of the app, everything works as expected. I am able to navigate using the main NavHostFragment to different master-detail screens. After I navigate to the master-detail screen, the nested NavHostFragment takes over and I can navigate screens in and out of the master detail fragment using the nested NavHostFragment.
When the user attempts to click the back button, which would cause the to leave the master detail screen and navigate to the previous screen, we pop up a dialog to the user asking if they really want to leave the screen (it's a screen where they enter a lot of data). To accomplish this we register an onBackPressDispatcher callback so we know when the back button was pressed and navigate to the dialog when the callback is invoked. In the devDebug version, the user begins by being at location A on the nav graph. If, when they are at location A, they click the back button, then we show a dialog fragment asking if the user really intends to leave the screen. If, instead, the user navigates from location A to location B and clicks back they are first navigated back to location A. If they click the back button again, the back press dispatcher callback is invoked and they are then shown the dialog fragment asking if they really intent to leave location A. So it seems that that the back button affects the back stack of the nested NavHostFragment until the nested NavHostFragment only has one fragment left. When only one fragment is left and the back button is clicked, the onBackPressDisapatcher callback is invoked. This is exactly the desired behavior. However, when I write an Instrumentation test with Fragment Scenario where I attempt to test the ParentFragment I have found that the back press behavior is different. In the test I use Fragment Scenario to launch ParentFragment, I then run a test where I do a navigation in the nested NavHostFragment. When I click the back button I expect that the nested nav host fragment will pop its stack. However, the onBackPressDispatcher callback is invoked immediately instead of after the nested nav host fragment has one fragment left on its stack.
I set some breakpoints in the NavHostFragment and it seems that when the tests are run, the NavHostFragment is not setup to intercept back clicks. Its enableOnBackPressed() method is always called with a flag set to false.
I don't understand what about the test setup is causing this behavior. I would think that the nav host fragment would intercept the back clicks itself until it only had one fragment left on its backstack and only then would the onBackPressDispatcher callback be invoked.
Am I misunderstanding how I should be testing this? Why does the onBackPressDispatcher's callback get called when the back button is pressed.
As seen in the FragmentScenario source code, it does not currently (as of Fragment 1.2.1) use setPrimaryNavigationFragment(). This means that the Fragment being tested does not intercept the back button and hence, its child fragments (such as your NavHostFragment) do not intercept the back button.
You can set this flag yourself in your test:
#Test
fun testParentFragment() {
// Use the reified Kotlin extension to launchFragmentInContainer
with(launchFragmentInContainer<ParentFragment>()) {
onFragment { fragment ->
// Use the fragment-ktx commitNow Kotlin extension
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
// Now you can proceed with your test
}

Navigation Component .popBackStack() with arguments

I have Two fragment. SecondFragment and ThirdFragment. Actually I use the Navigation Component for passing value between fragments. Like this:
SecondFragment:
val action = SecondFragmentDirections.action_secondFragment_to_thirdFragment().setValue(1)
Navigation.findNavController(it).navigate(action)
Here is how I read the value from the ThirdFragment:
arguments?.let {
val args = ThirdFragmentArgs.fromBundle(it)
thirdTextView.text = args.value.toString()
}
It's work fine. Now my stack is look like this:
ThirdFragment
SecondFragment
There is any option for pass value from the opened ThirdFragment to the previous SecondFragment with the new Navigation Component? (When ThirdFragment is finishing)
I know about onActivityResult, but If Nav.Component serve better solution than I want use that.
Thank you!
It's a bit late for this answer but someone may find it useful. In the updated versions of the navigation component library it is now possible to pass data while navigating back.
Suppose the stack is like this
FragmentA --> FragmentB.
We are currently now in FragmentB and we want to pass data when we go back to FragmentA.
Inside FragmentAwe can create an observer with a key:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
// Instead of String any types of data can be used
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
Then inside FragmentB if we change its value by accessing previous back stack entry it will be propagated to FragmentA and observer will be notified.
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "value that needs to be passed")
navController.popBackStack()
Just came across setFragmentResult(), pretty easy to use. The docs on this are here.
If you are navigating: Fragment A -> Fragment B -> Fragment A
Add this to fragment A:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
shouldUpdate = bundle.getBoolean("bundleKey")
}
}
Then in fragment B add this line of code:
setFragmentResult("requestKey", bundleOf("bundleKey" to "value to pass back"))
// navigate back toFragment A
When you navigate back to fragment A the listener will trigger and you'll be able to get the data in the bundle out.
What you are asking for is an anti-pattern. You should either
navigate to the second fragment again with the new values you would like to set
use the third fragment ins a separate activity and start it with startActivityForResult()
use a ViewModel or some kind of singleton pattern to hold on to your data (make sure you clear the data after you no longer need it)
these are some of the patterns that came to my mind. Hope it helps.
As described here:
When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated element. app:popUpTo tells the Navigation library to pop some destinations off of the back stack as part of the call to navigate(). The attribute value is the ID of the most recent destination that should remain on the stack.
<fragment
android:id="#+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="#layout/fragment_c">
<action
android:id="#+id/action_c_to_a"
app:destination="#id/a"
app:popUpTo="#+id/a"
app:popUpToInclusive="true"/>
</fragment>

Categories

Resources