I'm trying to find if a fragment is already in the backStack of my NavHostFragment (so it automatically manages the fragment transactions and backstack), in order to pop back to it when the user selects that destination from my Side Menu, instead of adding another new fragment to the backstack.
Here's the catch: Many of my fragments are the same Class, let's call it ArticleListFragment, and their contentId param (a simple string id) changes what is being displayed in those fragments.
This means I cannot use nav_host_fragment.childFragmentManager.findFragmentById() since multiple of my fragments in the backstack have the same fragment id.
What I've tried so far is this
var foundIndex = -1
for (i in 0 until nav_host_fragment.childFragmentManager.backStackEntryCount) {
val currFragmentTag = nav_host_fragment.childFragmentManager.getBackStackEntryAt(i).name
val currFragmentId = nav_host_fragment.childFragmentManager.getBackStackEntryAt(i).id
//val currFragment = nav_host_fragment.childFragmentManager.findFragmentByTag(currFragmentTag) // always returns null
val currFragment = nav_host_fragment.childFragmentManager.findFragmentById(currFragmentId) // always returns null
// currentFragment is null so the check always fails
if (currFragment is ArticleListFragment && currFragment.contentId == "a value I need to check") {
foundIndex = i
break
}
}
Am I doing something wrong? Is there another way to check if a fragment, added by the Android Navigation Component, is already in the back stack?
Well, I had the same issue, i can not say that this is a perfect solution but you can get it with
findNavController()
.backStack
.firstOrNull { it.destination.id == ID_YOU_WANT_TO_CHECK }
backstack is actually restricted so the method needs to be Suppressed.
#SuppressLint("RestrictedApi")
Related
I am using jetpack navigation graph in my android project. Normally to navigate one to another fragment we connect a link between them in nav graph and call like this
findNavController().navigate(R.id.action_navigation_login_to_navigation_send_code)
But We can call the fragment this way as well without creating the link.
findNavController().navigate(R.id.navigation_send_code)
What is the basic difference? In my sense, both do the same work.
Both versions back to the same navController navigate() function that takes in an integer resource id whether it's a fragment id or an action id.
Navigate to a destination from the current navigation graph. This supports both navigating
via an {#link NavDestination#getAction(int) action} and directly navigating to a destination.
Internally it calls this navigate() version where it examines whether it's an action id or not.
If it's an action id, then it gets the destination id from it; if not an action id it consider it as a destination id and continue on; notice the comments in below:
#IdRes int destId = resId;
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) { // here the destId is an action id
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId(); // resets the destId to the destination id
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
// Now destId is the destination fragment id.. continue on...
So, technically no difference, but adding an action id adds an extra step of getting the fragment id which is nothing in terms of scalable apps.
The other difference, that if you put an actionId instead of a fragment id, you'd have an extra feature of navOptions that can be returned from the navGraph like adding enter/exit animation.
I have this scenario in which the user clicks on a button in composable A then selects an item from the list from composable B and selects another item from the list from composable C.
My problem is when I select an item from screen C I want to navigate back to screen A with whatever I selected in B & C. But popBackStack doesn't work when arguments are given.
Here is the code,
navController.popBackStack(route = Screen.SelectPlan.route + "?regionId=${region.id}&operatorId=${operator.id}")
Right now, I see that popBackStack does take a route argument, but converts it to a hashcode to navigate back instead of creating a Uri-like navigate function.
I encountered the same issue and have just discovered the solution for myself.
When you navigate to a destination, using args like so:
val arg1 = "someValue"
val arg2 = "someOtherValue"
navController.navigate("Destination/$arg1/$arg2")
... this route is stored in the newly created backstack entry, not with the values of those args, but with their names as assigned in your NavHost.
Assume my NavHost contains a composable with a route of Destination/{arg1}/{arg2}.
If the .navigate() call in my previous example is executed, an entry will be added to the backstack with this route Destination/{arg1}/{arg2}, not this route Destination/someValue/someOtherValue.
You didn't provide your NavHost in the post, but if you replace the values of those args in the call to .popBackStack() with their names you assign in your NavHost, it should work for you.
Try put "/" instead of ?
navController.popBackStack(route = Screen.SelectPlan.route + "/regionId=${region.id}&operatorId=${operator.id}")
This is indeed a bug and it bothered me for a long time. After a long time of research and reading the navigation source code, I wrote my own extension function NavController.popBackStack to fix this problem. It is tricky but works for me. Also note that this version of extension function does not support the inclusive and saveState options (due to the current navigation API limitation), so you can't restoreState when you return to the current destination.
import android.content.Intent
import androidx.navigation.NavController
fun NavController.popBackStack(route: String): Boolean {
if (backQueue.isEmpty()) {
return false
}
var found = false
var popCount = 0
val iterator = backQueue.reversed().iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
popCount++
val intent = entry
.arguments
?.get("android-support-nav:controller:deepLinkIntent") as Intent?
if (intent?.data?.toString() == "android-app://androidx.navigation/$route") {
found = true
break
}
}
if (found) {
navigate(route) {
while (popCount-- > 0) {
popBackStack()
}
}
}
return found
}
I have an activity with two fragments. Either fragment is located on container in activity. Also activity has toolbar with settings button. I should pass on settings fragment using Navigation by click on settings button, but i have problems with it, because findNavController(R.id.home_nav).navigate(R.id.action_second_fragment_to_settings_fragment)
works if we pass to settings screen and second fragment is active, but from first active will be crash.
What do i need to do for solving this problem? I used just actions in Navigation component for passing between fragment. May be there is useful solution for this task
if your navController in MainActivity, with this code you can access to navController and send data to fragment.
load new fragment :
val bundle = Bundle()
bundle.putString("id", id)
(activity as MainActivity).navController.navigate(R.id.orderDetailFragment, bundle)
for get data in onViewCreated in fragment :
arguments?.let {
if (it.getSerializable("id") != null) {
val id = it.getString("id")
}
}
i hope it's useful
In the main fragment of my activity, I generate more fragments dependent on a config file.
When the device is rotated, I want to reuse the former generated fragments instead of creating them again.
This is the creation method, called in onCreateView of the main fragment:
private fun initChildFragments(elementsContainer: LinearLayout) {
val transaction = childFragmentManager.beginTransaction()
for (element in config.elements) {
val id = element.id
// create new container for fragment
addElementContainer(elementsContainer, id)
// try to re-use old fragment
var fragment = childFragmentManager.findFragmentByTag(element.getTag())
if (fragment == null) {
// create new fragment (should only happen at first run)
fragment = TestFragment.newInstance()
}
// add fragment to our container.
transaction.replace(id, fragment, element.getTag()
}
transaction.commitNow()
}
private fun addElementContainer(container: LinearLayout, id: Int) {
val childContainer = FrameLayout(context)
childContainer.id = id
container.addView(childContainer)
// for test purposes to make the container visible
val textView = TextView(context)
textView.setText("ID: " + id)
container.addView(textView)
}
When I first start the app, everything works fine. When I rotate the device, the fragments where found by findFragmentByTag and added to the containers. The element containers are visible on the screen (through the text view), but the fragments are not visible.
If I re-create the fragments every time the device rotates, they appear on the screen again, but then I loose all my ViewModel data.
I am in the midst adding a feature the my team's app that will allow the user to choose between 2 different home screens. Note that both of these screens have very different feels.
My current setup for this is to have 2 separate fragments Home1Fragment and Home2Fragment that both extend an abstract class called HomeFragment. The user will use Home1Fragment by default until they change the preference in the settings page.
I have both of these fragments implemented and both work properly if they act a lone as a home screen.
My problem resides when I try to switch them from one fragment to another. I've tried to this via
#Override
public void onBackPressed() {
FragmentManager fragmentManager = getFragmentManager();
if(fragmentManager.getBackStackEntryCount() > 1) {
fragmentManager.popBackStackImmediate();
int option = getSharedPreferences('**hidden**', MODE_PRIVATE).get('home_screen_option', 0);
Fragment fragment = fragmentManager.findFragmentById(R.id.content);
if(fragment != null && fragment instanceof HomeFragment) {
if(f instanceof Home1Fragment && option == HOME_2) {
f = new Home2Fragment();
} else if (f instanceof Home2Fragment && option == HOME_1) {
f = new Home1Fragment();
}
}
} else {
finish();
}
}
There may be typos in the code above because I wrote it on the fly without the real code in front of me.
You don't actually set the new fragment in your code.
But I would choose a different approach that's based on a saved state (e.g. a setting in shared preferences) and is handled by the home screen itself (instead of back stack manipulation which I advocate against).
Option 1.
Please think if your home screen can be a separate Activity. That way you can just load the correct fragment when the Activity starts based on a value stored in SharedPreferences.
Option 2.
Pretty much the same thing, just using a nested fragment. Let your home screen be a Fragment and add a container for child fragments in its layout. Then just use the child fragment manager to load the correct view accordingly.