So I have this navigation graph, and I'm implementing a deeplink to different fragments once the NEXT buton is pressed on the mainFragment.
However I encountered 2 problems:
If I call for example:
val deeplink = findNavController()
.createDeepLink()
.addDestination(R.id.fragmentNo5)
.createPendingIntent()
deeplink.send()
The app goes from mainFragment -> fragmentNo5, and when I click back it returns to mainFragment instead of fragmentNo2.
Then I tested opening the fragment8 which is not even in the path by actions, and it could still open it mainFragment -> fragmentNo8
I could even add more destinations to do something as the following, which should be illegal:
mainFragment -> fragmentNo8 -> otherFragment
So I see no purpose on using deeplink this way, or is it simply a way to construct the path that I need to still know and program?
val deeplink = findNavController()
.createDeepLink()
.addDestination(R.id.otherFragment)
.addDestination(R.id.fragmentNo1)
.addDestination(R.id.fragmentNo2)
.addDestination(R.id.fragmentNo3)
.createPendingIntent()
deeplink.send()
Related
Good day. So I've been working around with NavComponent of Jetpack for Android
I've thought that management of BackStack of fragments had to be implemented there already, well in fact it is there but I have faced an issue.
Here is my structure:
I have and entry Activity
I have a NavHost in the activity
I have Bottom Navigation bar in the Activity
For each Bottom Item I am using separate Fragments to navigate through.
Here is the code for the navigation.
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_home -> {
navController.apply {
navigate(R.id.navigation_home)
}
true
}
R.id.navigation_dashboard -> {
navController.apply {
navigate(R.id.dashboardFragment)
}
true
}
R.id.navigation_notifications -> {
true
}
else -> {
false
}
}
}
Never mind the last item.
So the issue is next.
If I try to switch between home and dashboard multiple times, when I press back then the stack surely will start popping all the items included there. So if I move like 6 times it will take me 12 attempts to actually exit the app.
Currently I couldn't find any source where for example the navigate() method will accept some sort of argument to cash my fragments instead of recreating it each time and adding to the BackStack.
So what kind of approach would you suggest?
If I to manage the BackStack manually on each back button pressed, what's the purpose of NavController at all? Just for creating and FORWARD navigation?
I think I'm missing some source in Android's official docs.
Thank you beforehand.
P.S.
using navController.popBackStack() before calling navigate() surely isn't the correct choice.
According to the documentation here :
NavigationUI can also handle bottom navigation. When a user selects a menu item, the NavController calls onNavDestinationSelected() and automatically updates the selected item in the bottom navigation bar.
to do so you have to give your bottom navigation items an ids as same as the corresponding destination in your navigation graph , and then tie you bottom view to the controller like this :
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
NavigationUI.setupWithNavController(bottomNav, navController);
Note : from my personal experience , when the startDestination in the graph , that start by default is not currently in back stack (In my case it was the landing page which i pop it out when going to home fragment) then the app act with weird behavior like this . so make sure the start destination is existed in your back stack on should work fine .
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
}
I have 2 fragment call CreateRoomFragment and DisplayPhotoFragment,the navigation graph is look like this:
<navigation>
<fragment
android:id="#+id/createRoomFragment"
android:name="package.room.CreateRoomFragment"
android:label="Create a room"
tools:layout="#layout/fragment_create_room">
<action
android:id="#+id/action_createRoomFragment_to_roomFragment"
app:destination="#id/roomFragment" />
<action
android:id="#+id/action_createRoomFragment_to_displayPhotoFragment"
app:destination="#id/displayPhotoFragment" />
</fragment>
<fragment
android:id="#+id/displayPhotoFragment"
android:name="package.fragment.DisplayPhotoFragment"
android:label="fragment_display_photo"
tools:layout="#layout/fragment_display_photo" >
<argument android:name="bitmap"
app:argType="android.graphics.Bitmap"/>
</fragment>
So when I wanna to move from CreateRoomFragment to DisplayPhotoFragment,I use the do as below:
NavDirections action = CreateRoomFragmentDirections.actionCreateRoomFragmentToDisplayPhotoFragment(selectedPhoto);
Navigation.findNavController(view).navigate(action);
Doing this,I can navigate to DisplayPhotoFragment.
But when I press back button of the device and also the Back arrow from the toolbar,it cant go back to CreateRoomFragment.
I tried this,but still unable to back to previous fragment:
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
navController.navigateUp(); //I tried this
navController.popBackStack(R.id.createRoomFragment,false); //and also this
}
});
Main Problem now:
By using the code above,the screen didnt go back to previous Fragment(CreateRoomFragment).It still stuck in DisplayPhotoFragment,but at the same time,an API method in CreateRoomFragment onViewCreated section is being called.
What causing this? and how can I solve this problem?
I had the same problem. For me the issue was that I was using a LiveData boolean to decide when to go to the next fragment. When I then navigated back/up the boolean was still true so it would automatically navigate forward again.
Android maintains a back stack that contains the destinations you've visited. The first destination of your app is placed on the stack when the user opens the app. Each call to the navigate() method puts another destination on top of the stack. Tapping Up or Back calls the NavController.navigateUp() and NavController.popBackStack() methods, respectively, to remove (or pop) the top destination off of the stack.
NavController.popBackStack() returns a boolean indicating whether it successfully popped back to another destination. The most common case when this returns false is when you manually pop the start destination of your graph.
When the method returns false, NavController.getCurrentDestination() returns null. You are responsible for either navigating to a new destination or handling the pop by calling finish() on your Activity.
When navigating using an action, you can optionally pop additional destinations off of the back stack by using popUpTo and popUpToInclusive parameter of the action.
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (true == conditionForCustomAction) {
CustomActionHere()
} else NavHostFragment.findNavController(this#MyFragment).navigateUp();
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, onBackPressedCallback
)
...
}
The best solution for handling navigation using live data is to use the SingleLiveEvent.
You can always use this class which is an extension of MutableLiveData.
https://gist.githubusercontent.com/cdmunoz/ebe5c4104dadc2a461f512ea1ca71495/raw/a17f76754f86a4c0b1a6b43f5c6e6d179535e627/SingleLiveEvent.kt
For a detail run down of this check:
https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70
Had a similar issue. We still have multiple activities with nav component.
So imagine activity A -> activity B, activity B has its own nav and fragments. When the initial fragment tries to pop the back stack there is nowhere to pop back to and the nav controller does not know to finish the activity. So one solution I found was to do
if (!findNavController().popBackStack()) activity?.finish()
If nav controller can not pop back it will finish activity.
You can use MutableSharedFlow instead on MutableLiveData if you want to observe the Event only once.
in your viewModel:
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() // read-only public view
suspend fun postEvent() {
_events.emit(event) // suspends until subscribers receive it
}
In your Activity/Fragment class:
lifecycleScope.launchWhenStarted {
viewModel.events.collect {
}
}
viewModel.postEvent()
This will prevent observing data continuously when going back to fragment.
I am trying to use the navigation controller right now. I want to move from LoginFragment to HomeFragment. In LoginFragment I use this code below to move to HomeFragment.
Navigation.findNavController(view).navigate(homeDestination)
However, when I tap the back button in the HomeFragment, it will go back to LoginFragment, I expect that when I tap the button it will close the app.
In old way, if I use activity instead of using Fragment, I usually do something like this to get that expected behaviour:
val intent = Intent(this,HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK.or(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
By using those flags, I use to get the expected behavior. But I don't how to implement the same behavior using the navigation controller.
Navigation offers a popUpTo and popUpToInclusive attributes for removing fragments from the back stack as part of a navigate() operation.
This can be set either in XML:
<!-- Add to your Navigation XML, then use navigate(R.id.go_home) -->
<action
android:id="#+id/go_home"
app:destination="#+id/home_fragment"
app:popUpTo="#+id/destination_to_pop"
app:popUpToInclusive="true"/>
Or set it programmatically:
NavOptions navOptions = new NavOptions.Builder()
.setPopUpTo(R.id.destination_to_pop, true)
.build();
Navigation.findNavController(view).navigate(homeDestination, navOptions)
You can also use the id of a <navigation> element as well.
I was following the answer of Ian but I had been having bad luck since I didn't know what the popUpTo would be.
So we have to use the id Of nav Graph there.
app:popUpTo="#+id/idOfNavGraph". //id of nav graph
<action
android:id="#+id/go_home"
app:destination="#+id/home_fragment"
app:popUpTo="#+id/idOfNavGraph". //id of nav graph
app:popUpToInclusive="true"/>
I think this should do the trick.
NavController controller = Navigation.findNavController(view);
controller.popBackStack(R.id.fragmentLogin, true);
controller.navigate(homeDestination)
Try this
val c = view.findNavController()
c.popBackStack() // current fragment will be pop up from the stack
c.navigate(DestinationFragmentID)
Obviously I'm late. Nonetheless I'd like to do my bit and expand Ian's answer. In app:popUpTo="#+id/destination_to_pop" the destination_to_pop really means the destination that you do not want to keep in the back stack. In your case, loging_fragment.
This way, when you navigate back from your HomeFragment the app will close since LoginFragment is not in the back stack anymore. You can find the related official documentation here.
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>