Multiple instances of same fragment: Android Navigation Component - android

I created a class to handle different back stacks for each tab inside my app, hence am using different nav controllers with a "currentcontroller" field to get the current one :
private val navNewsController: NavController = obtainNavHostFragment(fragmentTag = "news", containerId = R.id.newsTabContainer).navController.apply {
graph = navInflater.inflate(R.navigation.navigation_graph_main).apply {
startDestination = startDestinations.getValue(R.id.tab_news)
}
addOnDestinationChangedListener { controller, destination, arguments ->
onDestinationChangedListener?.onDestinationChanged(controller, destination, arguments)
}
}
val navFormController: NavController = obtainNavHostFragment(fragmentTag = "form", containerId = R.id.formTabContainer).navController.apply {
graph = navInflater.inflate(R.navigation.navigation_graph_main).apply {
startDestination = startDestinations.getValue(R.id.tab_form)
}
addOnDestinationChangedListener { controller, destination, arguments ->
onDestinationChangedListener?.onDestinationChanged(controller, destination, arguments)
}
}
private fun obtainNavHostFragment(
fragmentTag: String,
containerId: Int
): NavHostFragment {
val existingFragment = mainActivity.supportFragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
val navHostFragment = NavHostFragment.create(R.navigation.navigation_graph_main)
mainActivity.supportFragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNowAllowingStateLoss()
return navHostFragment
}
And when I switch tabs I just change the "currentController":
fun switchTab(tabId: Int, goToRoot: Boolean = false) {
currentFragment()?.onPause()
currentTabId = tabId
when (tabId) {
R.id.tab_news -> {
currentController = navNewsController
invisibleTabContainerExcept(newsTabContainer)
}
R.id.tab_form -> {
currentController = navFormController
invisibleTabContainerExcept(formTabContainer)
}
....
So I have this FragmentA that opens from both news and form.
Whenever I open FragmentA from news and then FragmentA from form, FragmentA from news gets reloaded with the new arguments opened from form.
I tried using different actions inside the nav graph, I tried declaring the fragment twice with different ids and then different actions for the respective ids. I also tried making "newsAFragment" and "formAFragment" by just extending the original "AFragemnt" and still doesnt' work.
I also tried nav options:
NavOptions.Builder().setLaunchSingleTop(false).build()
How can I use multiple instances of the same fragment class inside a nav graph?

Turns out the problem is with the ViewModel not the fragment itself.. It was using the same instance of the view model. Instead i know use a unique key for each instance from the viewmodelstore

Related

Issue with backstack and bottomnav in kotlin

I have a bottom nav with 4 fragments Home, Following, Notification, and Profile, there is no issue with the bottom navigation on backstack , but now for eg from profile fragment I jumped to a fragment called edit_profile which is not a part of the bottom nav and when press back I want that it should go back to the profile fragment but the backstack is taking me from edit_profile to directly home fragment
here is a recording link
I recently change my project from java to kotlin and I'm a beginner in kotlin
i really like the navigation of Pinterest and Instagram
Note:- All this code is automatically changed to kotlin (with some
changes done manually ) , this issue was also with java and not after migrating to kotlin , Also if you want more reference of the code
please tell me i will update the question
Code
MainActivity.kt // Bottom Nav
class MainActivity : AppCompatActivity() {
var bottomNavigationView: BottomNavigationView? = null
var integerDeque: Deque<Int> = ArrayDeque(3)
var flag = true
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main)
val window = this.window
window.statusBarColor = this.resources.getColor(R.color.black)
bottomNavigationView = findViewById(R.id.bottom_navigation_view)
integerDeque.push(R.id.nav_home)
loadFragments(Home_Fragment())
bottomNavigationView!!.selectedItemId = R.id.nav_home
bottomNavigationView!!.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
val id = item.itemId
if (integerDeque.contains(id)) {
if (id == R.id.nav_home) {
integerDeque.size
if (flag) {
integerDeque.addFirst(R.id.nav_home)
flag = false
}
}
integerDeque.remove(id)
}
integerDeque.push(id)
loadFragments(getFragment(item.itemId))
false
}
)
}
#SuppressLint("NonConstantResourceId")
private fun getFragment(itemId: Int): Fragment {
when (itemId) {
R.id.nav_home -> {
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
R.id.nav_following -> {
bottomNavigationView!!.menu.getItem(1).isChecked = true
return Following_Fragment()
}
R.id.nav_notification -> {
bottomNavigationView!!.menu.getItem(2).isChecked = true
return Notification_Fragment()
}
R.id.nav_profile -> {
bottomNavigationView!!.menu.getItem(3).isChecked = true
return Profile_Fragment()
}
}
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
private fun loadFragments(fragment: Fragment?) {
if (fragment != null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName)
.commit()
}
}
override fun onBackPressed() {
integerDeque.pop()
if (!integerDeque.isEmpty()) {
loadFragments(getFragment(integerDeque.peek()))
} else {
finish()
}
}
Edit_Profile.kt // from this fragment i want to go back to the last fragment which should be the profile fragment
class Edit_Profile : Fragment() {
private var profilePhoto: CircleImageView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_edit_profile, container, false)
profilePhoto = view.findViewById(R.id.circleImageView)
initImageLoader()
setProfileImage()
val imageView = view.findViewById<ImageView>(R.id.backArrow)
imageView.setOnClickListener {
val newCase: Fragment = Profile_Fragment()
assert(fragmentManager != null)
val transaction = requireFragmentManager().beginTransaction()
transaction.replace(R.id.fragment_container, newCase)
transaction.addToBackStack(Profile_Fragment.toString())
transaction.commit()
}
return view
}
Edit
added a part of the transaction from Profile Fragment to Edit Profile
ProfileFragment.kt
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
})
Now you are managing the back stack through the integerDeque array.
When you go to a new BottomNavigationView fragment; you added its id to the array if it doesn't already exist.
When you pop up the back stack; the fragment at the top is kicked off the array.
But since you pushed all those ids in the bottomNavigationView.setOnItemSelectedListener callback; then the integerDeque array only contains BottomNavigationView fragments ids.
And as the Edit_Profile fragment is not a part of BottomNavigationView fragments, then it won't be added/popped off the queue. Instead when you try to popup the back stack whenever the Edit_Profile fragment is shown; the normal behavior you manage in the onBackPressed() continues and the Profile_Fragment id will pop up from the queue making you return to the preceding fragment (Home_Fragment) in your mentioned example.
A little fix to this is to consider adding an id into the queue when you transact to Edit_Profile fragment so that this id is popped off the queue resulting in back to Profile_Fragment fragment.
You can do that with the fragment's id in order to make sure it's unique:
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
(requireActivity() as MainActivity).integerDeque.push(id) // <<<< pushing id to the queue
})
This should fix your problem.
Side tips:
Use setOnItemSelectedListener instead of setOnNavigationItemSelectedListener on the BNV as the latter is deprecated.
Return true instead of false from setOnItemSelectedListener callback as this should consume the event and mark the BNV as selected.
In Edit_Profile transaction replace the fragment instead of adding it with add as already the container is consumed; and this would make you avoid overlapping fragments in the container.
In onBackPressed(); you'd replace loadFragments(..) with bottomNavigationView.selectedItemId = integerDeque.peek(); this could be lighter to reuse the same fragment instead of redoing the transaction.
Usually I follow this pattern
Where I add HomeF in main container which includes all bottom nav tab, and all bottom nav tab will open in home container, and those fragment which are not part of bottom nav will open in main container. I generally add(not replace) all the fragments in main container and set add to back stack , so that if user goes from profile (home_container) to something in main container , while backstack we can pop the top fragment and user will be seeing profile.

Pass an argument to a nested navigation graph in Jetpack Compose

From the docs, I see you can nest navigation graphs like so:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
I am wondering, how would one pass an argument in the route, and make that available to all composables inside the nav graph?
Here's my current nav graph:
navigation(
// I'd like to grab this parameter
route = "dashboard?classId={classId}",
startDestination = Route.ScreenOne.route) {
composable(Route.ScreenOne.route) {
// And then pass the parameter here, or to any composable below
ScreenOne(classId)
}
composable(Route.ScreenTwo.route) {
ScreenTwo()
}
composable(Route.ScreenThree.route) {
ScreenThree()
}
}
I am basically trying to avoid setting the classId navigation argument individually on each composable route. I didn't see a way to pass a list of arguments to navigation() like you can in a composable().
It might be that what I am describing isn't possible, but looking forward to anyone's thoughts!
You can access the graph arguments from child composables:
navController.getBackStackEntry("dashboard?classId={classId}").arguments?.getString("classId")
From a quick test within a Hilt-based project, it looks like passing a property in as an argument to a navigation graph component results in the property being available in the savedStateHandle for any ViewModels that are made whilst that graph is in memory.
For example:
// 1. Define your routes.
sealed class Destination(val route: String) {
object TestGraph : Destination("TEST_GRAPH/{${Arguments.testParameter}}") {
object Arguments {
const val testParameter = "testParameter"
}
fun route(testParameter: String): String {
return "TEST_GRAPH/$testParameter"
}
object FirstScreen : Destination("FIRST_SCREEN")
}
}
// 2. Create a graph extension on NavGraphBuilder for the navigation graph.
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
navigation(
startDestination = Destination.TestGraph.FirstScreen.route,
route = Destination.TestGraph.route,
arguments = listOf(
navArgument(Destination.TestGraph.Arguments.testParameter) { type = NavType.StringType }
)
) {
composable(route = Destination.TestGraph.FirstScreen.route) {
FirstScreen()
}
}
}
// 3. Use the line below to navigate to this new graph.
navController.navigateTo(Destination.TestGraph.route("xyz"))
// 4. Access the savedStateHandle via the VM to get the parameter.
#Composable
fun FirstScreen(
viewModel: FirstScreenViewModel = hiltViewModel(),
) {
//...
}
#HiltViewModel
class FirstScreenViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
): ViewModel() {
private val testParameter: String = checkNotNull(savedStateHandle[Destination.TestGraph.Arguments.testParameter])
//...
}
I assume this works as the argument is created and maintained at the navigation graph-level. So, providing the graph is in memory, the property is accessible via savedStateHandle. If you were to pop this graph off of the navigation stack, I would expect the value to not be accessible anymore. Hope that helps!

NavGraph.addAll() to add nested nav graph progamatically not working

I want to have two versions of nav_graph in two build variant so and the first variant will use the main nav_graph and the second variant will add some more destinations to the main nav_graph so this makes the second graph duplicate of the main nav_graph but with more destinations which is hard to manage and keep update both at the same time so to solve this i go for dynamic navigation by creating a navigation graph by code which works fine but when i want to add a nested graph for second build variant destinations to the MainGraph it's not working
this file is in the main code base
MainGraph
object MainGraph{
var id_counter = 1
private val id = id_counter++
object Destinations {
val mainFragment = id_counter++
// ...
}
object Actions {
// ...
val to_settingsFragment = id_counter++
}
fun setGraph(context: Context, navController: NavController) {
navController.graph = navController.createGraph(id, Destinations.mainFragment) {
fragment<MainFragment>(Destinations.mainFragment) {
label = context.getString(R.string.app_name)
// ...
action(Actions.to_settingsFragment) {
destinationId = Destinations.settingsFragment
}
}
// ...
fragment<SettingsFragment>(Destinations.settingsFragment) {
label = context.getString(R.string.settings)
}
}
}
}
this file only in the second build variant
NestedGraph
object NestedGraph {
val id = MainGraph.id_counter++
object Destinations {
val nestedFirstFragment = MainGraph.id_counter++
val nestedSecondFragment = MainGraph.id_counter++
}
object Actions {
val to_nestedSecondFragment = MainGraph.id_counter++
}
fun addDestinations(navController: NavController) {
val navGraph = navController.createGraph(
this#NestedGraph.id,
Destinations.nestedFirstFragment
) {
fragment<NestedFirstFragment>(Destinations.nestedFirstFragment) {
action(Actions.to_nestedSecondFragment) {
destinationId = Destinations.nestedSecondFragment
}
}
fragment<NestedSecondFragment>(Destinations.nestedSecondFragment)
}
navController.graph.addAll(navGraph) // not working
}
}
After some debugging and try n fail i found that there is not wrong with NavGraph.addAll() method its working find it's just my mistake i was using the nested graph id instead of destination id when calling navigate and if anyone in future also get this kind of behaviour just make sure make the id variable private

Android FragmentManager and Fragment Result API

How can be that a fragment F which uses the new Fragment Result API to get results from 2 other fragments: A, B gets the result from A but not from B because B has a different parent FragmentManager (and I don't know why) ? How could be something like that ? 2 fragments called in the same way but they end up having same Activity but different FragmentManager ? The function calls are the following:
//THIS DOESN'T WORK. THE LISTENER IS NOT CALLED AFTER THE RESULT IS SET
private fun navigateToItemLocation() {
setFragmentResultListener(REQUEST_LOCATION_KEY) { s: String, bundle: Bundle ->
val locationId = bundle.getParcelable<ParcelUuid>(LOCATION_ID)!!.uuid
viewModel.viewModelScope.launch(Dispatchers.IO) {
val location = LocationRepository().get(locationId)!!
changeItemLocation(location)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationPagerToNavStockLocationSelection()
findNavController().navigate(action)
}
//THIS WORKS FINE:
private fun navigateToItemDetails(item: Item2) {
setFragmentResultListener(SELECTED_ITEM_KEY) { s: String, bundle: Bundle ->
val propertySetId = bundle.getParcelable<ParcelUuid>(SELECTED_ITEM_SET_ID)!!.uuid
clearFragmentResultListener(SELECTED_ITEM_KEY)
viewModel.viewModelScope.launch(Dispatchers.IO) {
val repository = PropertySetRepository()
val propertySet = repository.get(propertySetId)!!
val propertySetInfo = ItemFactory.loadPropertySetInfo(propertySet)
withContext(Dispatchers.Main) { setPackageCode(null) }
selectItem(item.item, propertySetInfo, item.description, null)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationToNavStockItemDetails(ParcelUuid(item.item.id), true)
findNavController().navigate(action)
}
Both fragments A and B are in a separate Dynamic Feature. The only single problem I have is that when the following function is called:
fun onSelect() {
viewModel.pickedLocation.value = (viewModel.selectedLocation as? LocationExt2?)?.location
val result = bundleOf(Pair(LOCATION_ID, ParcelUuid(viewModel.pickedLocation.value!!.id)))
setFragmentResult(REQUEST_LOCATION_KEY, result)
findNavController().popBackStack()
}
setFragmentResult(REQUEST_LOCATION_KEY, result)
Doesn't produce any result because the FragmentManager is not the same of the calling Fragment. The same method in fragment A which is:
private fun onSetSelected(id: UUID) {
propertySets.removeObservers(viewLifecycleOwner)
adapter.tracker = null
setFragmentResult(SELECTED_ITEM_KEY, bundleOf(Pair(SELECTED_ITEM_SET_ID, ParcelUuid(id))))
findNavController().popBackStack()
}
As a temporarily workaround I replaced the call to Fragment's FragmentManager with Activity.supportFragmentManager.setFragmentResultListener. It works but still I do not understand why fragments A and B behave differently...
Check that fragment where you listen for fragment result and fragment where you set the result are in the same fragment manager.
Common case where this would happen is if you are using Activity.getSupportFragmentManager() or Fragment.getParentFragmentManager() alongside Fragment.getChildFragmentManager().
Check this blog article for the principles and rules with Fragment Result API on medium: https://medium.com/#FrederickKlyk/state-of-the-art-communication-between-fragments-and-their-activity-daa1fe4e014d
Only one listener can be registered for a specific request key.
If more than one listener is registered on the same key, the previous one will be replaced by the newest listener.

Android Navigation Architecture Component - Get current visible fragment

Before trying the Navigation component I used to manually do fragment transactions and used the fragment tag in order to fetch the current fragment.
val fragment:MyFragment = supportFragmentManager.findFragmentByTag(tag):MyFragment
Now in my main activity layout I have something like:
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/nav_host"
app:navGraph= "#navigation/nav_item"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost= "true"
/>
How can I retrieve the current displayed fragment by the Navigation component?
Doing
supportFragmentManager.findFragmentById(R.id.nav_host)
returns a NavHostFragment and I want to retrieve my shown 'MyFragment`.
Thank you.
I managed to discover a way for now and it is as follows:
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host);
navHostFragment.getChildFragmentManager().getFragments().get(0);
In case of course you know it is the first fragment. I am still investigating a way without this. I agree it is not the best way but that should be something for now.
There is no way I can find to retrieve the current fragment instance. However, you can get the ID of lastly added fragment using the code below.
navController.currentDestination?.getId()
It returns the ID in navigation file. Hope this helps.
You should look the childFragmentManager primaryNavigationFragment property of your nav fragment, this is where the current displayed fragment is referenced.
val navHost = supportFragmentManager.findFragmentById(R.id.main_nav_fragment)
navHost?.let { navFragment ->
navFragment.childFragmentManager.primaryNavigationFragment?.let {fragment->
//DO YOUR STUFF
}
}
}
This seems like the cleanest solution. Update: Changed extension function to property. Thanks Igor Wojda.
1. Create the following extension
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
val FragmentManager.currentNavigationFragment: Fragment?
get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first()
2. In Activity with NavHostFragment use it like this:
val currentFragment = supportFragmentManager.currentNavigationFragment
Use addOnDestinationChangedListener in activity having NavHostFragment
For Java
NavController navController= Navigation.findNavController(MainActivity.this,R.id.your_nav_host);
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
#Override
public void onDestinationChanged(#NonNull NavController controller, #NonNull NavDestination destination, #Nullable Bundle arguments) {
Log.e(TAG, "onDestinationChanged: "+destination.getLabel());
}
});
For Kotlin
var navController :NavController=Navigation.findNavController(this, R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->
Log.e(TAG, "onDestinationChanged: "+destination.label);
}
this might be late but for anyone who's still looking
val currentFragment = NavHostFragment.findNavController(nav_host_fragment).currentDestination?.id
then you can do something like
if(currentFragment == R.id.myFragment){
//do something
}
note that this is for kotlin, java code should be almost the same
My requirement is to get the current visible fragment from Parent activity,my solution is
private Fragment getCurrentVisibleFragment() {
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().getPrimaryNavigationFragment();
FragmentManager fragmentManager = navHostFragment.getChildFragmentManager();
Fragment fragment = fragmentManager.getPrimaryNavigationFragment();
if(fragment instanceof Fragment ){
return fragment ;
}
return null;
}
This may help some one with same problem.
navController().currentDestination?.id
will give you your current Fragment's id where
private fun navController() = Navigation.findNavController(this, R.id.navHostFragment)
this id is the id which you have given in your Navigation Graph XML file under fragment tag. You could also compare currentDestination.label if you want.
First, you get the current fragment by id, then you can find the fragment, depending on that id.
int id = navController.getCurrentDestination().getId();
Fragment fragment = getSupportFragmentManager().findFragmentById(id);
Found working solution.
private fun getCurrentFragment(): Fragment? {
val currentNavHost = supportFragmentManager.findFragmentById(R.id.nav_host_id)
val currentFragmentClassName = ((this as NavHost).navController.currentDestination as FragmentNavigator.Destination).className
return currentNavHost?.childFragmentManager?.fragments?.filterNotNull()?.find {
it.javaClass.name == currentFragmentClassName
}
}
As per #slhddn answer and comment this is how I'm using it and retrieving the fragment id from the nav_graph.xml file:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val navController = findNavController(this, R.id.nav_host_fragment)
ivSettingsCog.setOnClickListener {
if (navController.currentDestination?.id == R.id.updatesFragment) // Id as per set up on nav_graph.xml file
{
navController.navigate(R.id.action_updatesFragment_to_settingsFragment)
}
}
}
}
Using navigation components, I got the current displayed fragment using this line of code:
val fragment = parentFragmentManager.primaryNavigationFragment
From an Activity that uses NavHostFragment, you can use the code below to retrieve the instance of the Active Fragment.
val fragmentInstance = myNavHostFragment?.childFragmentManager?.primaryNavigationFragment
Within your activity, hosting the NavHostFragment we can write the following piece of code to fetch the instance of the currently displayed fragment
Fragment fragment = myNavHostFragment.getChildFragmentManager().findFragmentById(R.id.navHostFragmentId)
if(fragment instanceOf MyFragment) {
//Write your code here
}
Here R.id.navHostFragmentId is the resId for the NavHostFragment within your activity xml
Make :
• in some button in your fragment:
val navController = findNavController(this)
navController.popBackStack()
• In your Activity onBackPressed()
override fun onBackPressed() {
val navController = findNavController(R.id.nav_host_fragment)
val id = navController.currentDestination?.getId()
if (id == R.id.detailMovieFragment){
navController.popBackStack()
return
}
super.onBackPressed()
}
The first fragment in navhost fragment is the current fragment
Fragment navHostFragment = getSupportFragmentManager().getPrimaryNavigationFragment();
if (navHostFragment != null)
{Fragment fragment = navHostFragment.getChildFragmentManager().getFragments().get(0);}
Finally the working solution for those of you who are still searching
Actually it is very simple and you can achieve this using callback.
follow these steps:
Create a listener inside the fragment you want to get the instance from activity onCreate()
public class HomeFragment extends Fragment {
private OnCreateFragment createLIstener;
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
createLIstener.onFragmentCreated(this);
View root = inflater.inflate(R.layout.fragment_home, container, false);
//findViews(root);
return root;
}
#Override
public void onCreateOptionsMenu(#NonNull Menu menu, #NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_main, menu);
}
#Override
public boolean onOptionsItemSelected(#NonNull MenuItem item) {
if (item.getItemId()==R.id.action_show_filter){
//do something
}
return super.onOptionsItemSelected(item);
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
createLIstener = (OnCreateFragment) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement OnArticleSelectedListener");
}
}
public interface OnCreateFragment{
void onFragmentCreated(Fragment f);
}
}
Implement your listener in the host Fragment/Activity
public class MainActivity extends AppCompatActivity implements HomeFragment.OnCreateFragment {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home,
R. ----)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView, navController);
}
#Override
public void onFragmentCreated(Fragment f) {
if (f!=null){
H.debug("fragment created listener: "+f.getId());
f.setHasOptionsMenu(true);
}else {
H.debug("fragment created listener: NULL");
}
}
}
And that is everything you need to do the job.
Hop this help someone
The best way I could figure out with 1 fragment, Kotlin, Navigation usage:
On your layout file add the tag: "android:tag="main_fragment_tag""
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/nav_host"
app:navGraph= "#navigation/nav_item"
android:tag="main_fragment_tag"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost= "true"
/>
On your Activity:
val navHostFragment = supportFragmentManager.findFragmentByTag("main_fragment_tag") as NavHostFragment
val mainFragment = navHostFragment.childFragmentManager.fragments[0] as MainFragment
mainFragment.mainFragmentMethod()
Here is my solution for java
NavHostFragment navHostFragment= (NavHostFragment) getSupportFragmentManager().getFragments().get(0);
MyFragment fragment = (MyFragment) navHostFragment.getChildFragmentManager().getFragments().get(0);
fragment.doSomething();
create extension function on Fragment Manager
inline fun <reified T : Fragment> FragmentManager.findNavFragment(hostId: Int): T? {
val navHostFragment = findFragmentById(hostId)
return navHostFragment?.childFragmentManager?.fragments?.findLast {
it is T
} as T?
}
Now simply use it by giving id of the hosting fragment and get the specified fragment
val myFragment: MyFragment? = supportFragmentManager.findNavFragment<MyFragment>(R.id.nav_host_id)
can you please check with getChildFragmentManager() call with
NavHostFragment fragment = supportFragmentManager.findFragmentById(R.id.nav_host);
MyFragment frag = (MyFragment) fragment.getChildFragmentManager().findFragmentByTag(tag);
If you need the instance of your fragment, you can go with:
(Activity) => supportFragmentManager.fragments.first().childFragmentManager.fragments.first()
It's very ugly but from there you can check if it's an instance of the fragment you want to play around with
On Androidx, I have tried many methods for finding out required fragment from NavHostFragment. Finally I could crack it with below method
public Fragment getFunctionalFragment (String tag_name)
{
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
FragmentManager navHostManager = Objects.requireNonNull(navHostFragment).getChildFragmentManager();
Fragment fragment=null;
List fragment_list = navHostManager.getFragments();
for (int i=0; i < fragment_list.size() ; i ++ )
{
if ( fragment_list.get(i) instanceof HomeFragment && tag_name.equals("frg_home")) {
fragment = (HomeFragment) fragment_list.get(i);
break;
}
}
return fragment;
}
This code works from me
NavDestination current = NavHostFragment.findNavController(getSupportFragmentManager().getPrimaryNavigationFragment().getFragmentManager().getFragments().get(0)).getCurrentDestination();
switch (current.getId()) {
case R.id.navigation_saved:
// WRITE CODE
break;
}
this might be late but may help someone later.
if you are using nested navigations you can get id like this in Kotlin
val nav = Navigation.findNavController(requireActivity(), R.id.fragment_nav_host).currentDestination
and this is one of fragments in fragment_nav_host.xml
<fragment
android:id="#+id/sampleFragment"
android:name="com.app.sample.SampleFragment"
android:label="SampleFragment"
tools:layout="#layout/fragment_sample" />
and you can check all you need like this
if (nav.id == R.id.sampleFragment || nav.label == "SampleFragment"){}
val fragment: YourFragment? = yourNavHost.findFragment()
Note: findFragment extension function is located in androidx.fragment.app package, and it's generic function
I combined Andrei Toaders answer and Mukul Bhardwajs answer with an OnBackStackChangedListener.
As an attribute:
private FooFragment fragment;
In my onCreate
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host);
FragmentManager navHostManager = Objects.requireNonNull(navHostFragment).getChildFragmentManager();
FragmentManager.OnBackStackChangedListener listener = () -> {
List<Fragment> frags = navHostManager.getFragments();
if(!frags.isEmpty()) {
fragment = (FooFragment) frags.get(0);
}
};
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
if(destination.getId()==R.id.FooFragment){
navHostManager.addOnBackStackChangedListener(listener);
} else {
navHostManager.removeOnBackStackChangedListener(listener);
fragment = null;
}
});
This way i can get a fragment, which will be displayed later on.
One even may be able to loop though frags and check multiple fragments with instanceof.
I need to do this to setup a bottom navigation view and I did it this way:
val navController = Navigation.findNavController(requireActivity(), R.id.nav_tabs_home)
val bottomNavBar: BottomNavigationView = nav_content_view
NavigationUI.setupWithNavController(bottomNavBar, navController)
The best way to achieve this is to register a fragment lifecycle callback.
As per the original question, if your NavHostFragment is defined as...
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/nav_host"
app:navGraph= "#navigation/nav_item"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost= "true"
/>
Then in your main activity onCreate(..)...
nav_host.childFragmentManager.registerFragmentLifecycleCallbacks(
object:FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager, f: Fragment, v: View,
savedInstanceState: Bundle?
) {
// callback method always called whenever a
// fragment is added, or, a back-press returns
// to the start-destination
}
}
)
Other callback methods may be overridden to fit your requirements.
This solution will work in most situations
Try this:
val mainFragment = fragmentManager?.primaryNavigationFragment?.parentFragment?.parentFragment as MainFragment
or this:
val mainFragment = fragmentManager?.primaryNavigationFragment?.parentFragment as MainFragment

Categories

Resources