I have this code in my Activity:
binding.imageViewDot.setOnClickListener {
findNavController(this, R.id.navHostFragmentApp).navigate(R.id.syncFragment)
}
}
When clicking the button it navigates to this fragment. But each time the user click it it adds another one. How can I set it to only once. For example, if fragment is visible do not run this code. For example:
binding.imageViewDot.setOnClickListener {
if (//fragment not visible)
{
findNavController(this, R.id.navHostFragmentApp).navigate(R.id.syncFragment)
}
}
You can use currentDestination to get the visible Fragment and check it's id before navigating
val navController = findNavController(this, R.id.navHostFragmentApp)
if(navController.currentDestination?.getId() != R.id.syncFragment){
navController.navigate(R.id.syncFragment)
}
Related
When I'm entering MessageDetails fragment, leaving with back arrow and navigating to other fragment, getting back and trying to click on button to MessageDetails again I got crash and error:
"Fragment not associated with a fragment manager".
fun handleEvent(event: MessageListEvent) {
when (event) {
is NavToMessageDetails -> {
val action =
MessageListFragmentDirections.actionMessageListFragmentToMessageDetailsFragment(
event.id,
DateHelper.parseFullDate(event.date),
event.message,
event.title
)
findNavController().navigate(action)
}
}
This is the fragment function which is responsible for navigating to the details fragment. In app we have created BottomNavigation with app:menu values and this error occurs when navigating between those fragments.
Event (MessageListEvent) was logged right under function declaration and everything is alright with it. Also, when I'm trying to wrap findNavController() with lifecycleScope.launchWhenResumed {} it won't navigate or do anything with it.
I have anAndroid navigation drawer with a menu that tapping on items open fragments. Good so far.
Some of those fragments need to open a new one for more details. I do that using this code from the first fragment:
getParentFragmentManager().beginTransaction().replace(R.id.nav_host_fragment_content_main, new MySecondFragment(), null).commit();
Tappin on the backbutton (the backarrow on the top-left corner) doesn't take me back to MyFirstFragment. It throughs an exceptions:
java.lang.IllegalStateException: Fragment MyFirstFragment not
associated with a fragment manager.
I can use this other code:
getParentFragmentManager().beginTransaction().add(R.id.nav_host_fragment_content_main, new MySecondFragment(), null).commit();
But with this code both fragments are visible at the same time. I mean, MySecondFragment views are displayed but I see in the background MyFirstFragment views.
Any idea on how I can open MySecondFragment from MyFirstFragment , and then, if the back arrow is pressed I want to return to the same place MyFirstFragment was before.
Thanks
NEW ANSWER:
Using the navigation drawer, it seems the fragment transactions happen under the NavHostFragment and its FragmentTransactionManager.
This forces us to get its childFragmentManager() and use it for checking the backStackEntryCount.
Therefore just adding the nested fragment to the parent backstack is not enough. We would need to override onBackPressed and onSupportNavigateUp and take into account the backstack of the NavHostFragment when going back and poping it.
private fun handleNestedFragmentsBackStack(): Boolean {
val navHostChildFragmentManager = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment_content_main)?.childFragmentManager
return if (navHostChildFragmentManager?.backStackEntryCount!! > 1) {
navHostChildFragmentManager.popBackStack()
false
} else {
val navController = findNavController(R.id.nav_host_fragment_content_main)
navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
override fun onBackPressed() {
handleNestedFragmentsBackStack()
}
override fun onSupportNavigateUp(): Boolean {
return handleNestedFragmentsBackStack()
}
Now when adding your second fragment to the parent's backstack, remember that the parent isn't the Activity, but the NavHostFragment.
You can proceed by doing the fragment transaction as suggested bellow in the initial answer.
INITIAL ANSWER:
Should work for standard fragment transactions
Adding your fragment to the backstack should solve your issue
FragmentTransaction ft = getParentFragmentManager().beginTransaction()
ft.replace(R.id.nav_host_fragment_content_main, new MySecondFragment(), null)
ft.addToBackStack(MySecondFragment.class.getName()) // you can use a string here, using the class name is just convenient
ft.commit();
When pressing the back button, you will navigate through the back stack.
The same in Kotlin code:
parentFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, MySecondFragment())
addToBackStack(MySecondFragment::class.java.name)
}
Filip Petrovski solution In Java code:
In the AppCompatActivity with the Navigation Drawer:
#Override
public boolean onSupportNavigateUp() {
FragmentManager oChildFragmentManager = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_content_main).getChildFragmentManager();
if(oChildFragmentManager.getBackStackEntryCount() > 1){
oChildFragmentManager.popBackStack();
return true;
}
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
return NavigationUI.navigateUp(navController, mAppBarConfiguration) || super.onSupportNavigateUp();
}
#Override
public void onBackPressed() {
FragmentManager oChildFragmentManager = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_content_main).getChildFragmentManager();
if(oChildFragmentManager.getBackStackEntryCount() > 1){
oChildFragmentManager.popBackStack();
return;
}
super.onBackPressed();
}
Any time you open a fragment do not forget addToBackStack:
getParentFragmentManager().beginTransaction().replace(R.id.nav_host_fragment_content_main, new MyNewFragment()).addToBackStack("").commit();
But with this code both fragments are visible at the same time.
You can set background color to its root layout.xml
Also you can use fragmentTransaction.addToBackStack(null) to add this transaction to the back stack
I have code below that adds random fragments(First,SecondFragment or ThirdFragment)
private fun addFragment() {
activeFragment = if (activeFragment is FirstFragment) {
SecondFragment()
} else if (activeFragment is SecondFragment) {
ThirdFragment()
} else {
FirstFragment()
}
fragmentTransaction = fragmentManager!!.beginTransaction()
fragmentTransaction?.add(R.id.fragment_container, activeFragment!!)
?.addToBackStack(null)
?.commit()
}
override fun onBackPressed() {
val fragment = fragmentManager?.findFragmentById(R.id.fragment_container)
if (fragment != null) {
Log.d("mytag", "onBackPressed: $fragment")
fragmentManager?.popBackStack()
return
}
super.onBackPressed()
}
In the code above everything works fine. I mean when i press back button it pops the last fragment and shows previous if there any.
But if i remove return statement inside if check (in onBackPressed function), then it pops 2 fragments at once from back stack.(I added onBackStackChangedListener and i can see how many items are left when i pop, so if there are 3 fragments i back stack when i pop it removes 2 fragments and only 1 remains, and then if i click back again, it removes last fragment and also destroys the app)
The reason you're seeing this behavior is because Fragments already handle the system back button when you use addToBackStack() as part of the super.onBackPressed() call.
That means you don't need to override onBackPressed() at all when using fragments.
I am working with navigation component and bottom navigation
val navController = indNavController(R.id.nav_host_fragment)
bottom_navigation.inflateMenu(R.menu.bottom_navigation_menu)
bottom_navigation.setupWithNavController(navController)
and I am facing the next issue:
When an item is selected in the bottom navigation, then a fragment is loaded. The problem comes when I press again the same item, then a new fragment will be loaded, which it does not make sense at all.
Example:
1- User selects menu item A, then FragmentA is loaded.
2- User selects again menu item A, then a new FragmentA will be loaded,
I was trying to use
bottom_navigation.setOnNavigationItemSelectedListener { }
But then the bottom navigation will not work with the navController.
So the question is: there is a way to handle this situation in order to load again a new fragment when the user is in that screen already?
Finally, I was able to fix this issue.
As I said before, this code:
bottom_navigation.setupWithNavController(navController)
is using
bottom_navigation.setOnNavigationItemSelectedListener { }
so every time I select / reselect an item, the navController will load a new fragment. I checked the javadoc for setOnNavigationItemSelectedListener() and it says:
Set a listener that will be notified when a bottom navigation item is selected. This listener * will also be notified when the
currently selected item is reselected, unless an {#link *
OnNavigationItemReselectedListener} has also been set.
So what I did is to add the next line to my code:
bottom_navigation.setOnNavigationItemReselectedListener { }
and that's all. The navController will load a fragment when an item is selected but not when an item is reselected in the bottom navigation.
I prefer to use the listener from navController:
navController.addOnDestinationChangedListener {
controller, destination, arguments ->
//destination.id for id fragment
}
So the listener is triggered when the destination changes - not by clicking bottom_navigation.
Because setOnNavigationItemSelectedListener is already used when setupWithNavController is declared.
Try this to ignore the user's click on the same selected item:
bottom_navigation.apply {
setOnNavigationItemSelectedListener {
if (it.itemId == bottom_navigation.selectedItemId) {
true
} else when (it.itemId) { ... }
when you use bottom_navigation.setOnNavigationItemSelectedListener { } before bottom_navigation.setupWithNavController(navController) the OnNavigationItemSelectedListener is overrided inside setupWithNavController function. So use
navController.addOnDestinationChangedListener {
controller, destination, arguments ->
//destination.id for id fragment
}
I'm studying menu and toolbar/actionbar. I'm trying the way: one activity container and many fragments. I created and setup a toolbar as actionbar for MainActivity (onCreate):
val myToolbar = this.findViewById<Toolbar>(R.id.myToolbar)
setSupportActionBar(myToolbar)
Then, I add itens by normal way with onCreateOptionMenu and handle click with onOptionsItemSelected
When a call Fragment 1, I change action bar and add back button like this (onCreate):
val actBar = (activity as AppCompatActivity).supportActionBar
actBar?.setDisplayHomeAsUpEnabled(true)
actBar?.setDisplayShowHomeEnabled(true)
actBar?.setDisplayUseLogoEnabled(false)
actBar?.title = "Fragment 1 toolbar"
actBar?.subtitle = ""
setHasOptionsMenu(true)
Then from Fragment 1, the Fragment 2 is called and setup as same way:
To handle back button click in fragments, in onOptionsItemSelected:
return if (item.itemId == android.R.id.home) {
activity?.onBackPressed()
true
} else return when (item?.itemId){
...
}
else -> super.onOptionsItemSelected(item)
}
And override onBackPressedin MainActivity:
override fun onBackPressed() {
if(supportFragmentManager.backStackEntryCount > 0){
supportFragmentManager.popBackStackImmediate()
}else{
super.onBackPressed()
}
}
The problem is: if I click on back button, it's backing as expected but the last object of action bar is showed. In MainActivity, only action itens are showed as expected:
How I can sync the bar according fragment and activity?
Note:
I'm using Kotlin, but Java solution are welcome (to convert to kotlin
later)
The fragments are added to back stack
I found a solution. I leave here for whoever interests:
I applied OnBackStackChangedListener that watch changes on back stack. Then, You can make any changes on UI.
supportFragmentManager.addOnBackStackChangedListener {
//UI changes
}
Inside, I check if has some fragment current using the fragment container:
supportFragmentManager.addOnBackStackChangedListener {
val currentFragment = supportFragmentManager.findFragmentById(R.id.you_fragment_container)
if (currentFragment == null){
//rebuild action bar here or make any another changes
}
}
In my case, I compare null that mean container has no fragment. So, if null, the root activity is on screen.
This can be used to make changes for any fragment you want to.
That's it.