Android Navigation Architecture Component - Get current visible fragment - android

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

Related

Error using <FragmentContainerView>, but no error with <fragment>

I am developing a mobile app and I am currently trying to rework my workflow to more appropriately leverage Activities and Fragments for their intended purposes and I have run across a strange issue I can't figure out. I have a fragment I am trying to add to an Activity, but what I try and use FragmentContainerView, the app crashes on launch, but it doesn't happen when I just use a tag with all the same attributes. In looking at the Logcat, the error comes from null being assigned to the last line of the utils file where it tries to assign to topAppBar view the view with an id of top_app_bar. Here is the relevant code:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fragmentManager.beginTransaction().replace(R.id.frame_layout, HomeFragment()).commit()
utils = Utils(this)
topAppBar = findViewById(R.id.top_app_bar)
drawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.navigation_view)
topAppBar.setNavigationOnClickListener {
if (!drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.openDrawer(GravityCompat.START)
}
else {
drawerLayout.closeDrawer(GravityCompat.START)
}
}
navigationView.setNavigationItemSelectedListener { item ->
val id: Int = item.itemId
drawerLayout.closeDrawer(GravityCompat.START)
when (id) {
R.id.navigation_home -> { utils.replaceFragment(HomeFragment(), getString(R.string.app_name)) }
R.id.navigation_recipes -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_budget -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_inventory -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_customers -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_reports -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
}
true
}
activity_main.xml
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.bakingapp.ui.TopAppBarFragment"
tools:layout="#layout/fragment_top_app_bar" />
Utils.kt (There is more to this file, but it is not relevant to the problem)
class Utils(activity: Activity) {
private var currentActivity: Activity
private var fragmentManager: FragmentManager
private var topAppBar: MaterialToolbar
val activitiesList = listOf("recipes", "budget", "inventory", "customers",
"reports")
init {
currentActivity = activity
fragmentManager = (activity as AppCompatActivity).supportFragmentManager
topAppBar = currentActivity.findViewById(R.id.top_app_bar)
}
}
This works perfectly fine when I use instead of what is currently there, but I get warnings saying I shouldn't use fragment. I should be able to use the more proper tag, but I don't understand why it can't find the view when I use this method, but it can find the view when I use the tag. If someone could explain what is happening here and what I can do to fix the issue, I would really appreciate it.
Step 1 : Add FragmentContainerView to your activity xml
<androidx.fragment.app.FragmentContainerView
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Step 2 : In your MainActivity.class file, declare FragmentManager
private FragmentManager manager;
Step 3 : Initialize FragmentManager in onCreate()
manager = getSupportFragmentManager();
Step 4 : In your onOptionsItemSelected() begin this fragment
Bundle bundle = new Bundle();
manager.beginTransaction()
.replace(R.id.container/*Your View Id*/, YourFragment.class, bundle, "TAG")
.setReorderingAllowed(true)
//.setCustomAnimations(R.anim.anim_enter, R.anim.anim_exit)
.addToBackStack("TAG")
.commit();
Have you tried putting FragmentContainerView inside a layout? Instead of using it as parent layout.
That could solve it.
I managed to figure out the problem. It was a scope issue. A lot of logic for a Fragment I was handling in MainActivity. I moved pretty much everything in my onCreate function into the relevant Fragment file and from there was able to get the FragmentContainerView working after refactoring this code

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.

Multiple instances of same fragment: Android Navigation Component

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

Stop fragment refresh in bottom nav using navhost

This problem has been asked a few times now, but we are in 2020 now, did anyone find a good usable solution to this yet?
I want to be able to navigate using the bottom navigation control without refreshing the fragment each time they are selected. Here is what I have currently:
navigation/main.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main"
app:startDestination="#id/home">
<fragment
android:id="#+id/home"
android:name="com.org.ftech.fragment.HomeFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/news"
android:name="com.org.ftech.fragment.NewsFragment"
android:label="News"
tools:layout="#layout/fragment_news"/>
<fragment
android:id="#+id/markets"
android:name="com.org.ftech.fragment.MarketsFragment"
android:label="Markets"
tools:layout="#layout/fragment_markets"/>
<fragment
android:id="#+id/explore"
android:name="com.org.ftech.ExploreFragment"
android:label="Explore"
tools:layout="#layout/fragment_explore"/>
</navigation>
activity_mail.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="#color/nav"
app:itemTextColor="#color/nav"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/main">
</com.google.android.material.bottomnavigation.BottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
app:menu="#menu/main"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="#+id/navigationView"
android:layout_gravity="start">
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private var drawerLayout: DrawerLayout? = null
private var navigationView: NavigationView? = null
private var bottomNavigationView: BottomNavigationView? = null
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer_layout)
navigationView = findViewById(R.id.navigationView)
bottomNavigationView = findViewById(R.id.bottomNavigationView)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(setOf(R.id.markets, R.id.explore, R.id.news, R.id.home), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
findViewById<NavigationView>(R.id.navigationView)
.setupWithNavController(navController)
findViewById<BottomNavigationView>(R.id.bottomNavigationView)
.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
startActivity(Intent(applicationContext, SearchableActivity::class.java))
}
return super.onOptionsItemSelected(item)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.options_menu, menu)
return super.onCreateOptionsMenu(menu)
}
}
In the fragment I am making a few calls to my services to fetch the data in onCreateView, when resuming the fragment I am assuming those calls will not longer be executed and the state of the fragment should be preserved.
Try this:
public class MainActivity extends AppCompatActivity {
final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();
}
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return true;
case R.id.navigation_dashboard:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
return true;
case R.id.navigation_notifications:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
return true;
}
return false;
}
};
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}
Or You can follow Google's recommended solution: Google Link
The simple solution to stop refreshing on multiple clicks on the same navigation item could be
binding.navView.setOnNavigationItemSelectedListener { item ->
if(item.itemId != binding.navView.selectedItemId)
NavigationUI.onNavDestinationSelected(item, navController)
true
}
where binding.navView is the reference for BottomNavigationView using Android Data Binding.
Kotlin 2020 Google's Recommended Solution
Many of these solutions call the Fragment constructor in the Main Activity. However, following Google's recommended pattern, this is not needed.
Setup Navigation Graph Tabs
Firstly create a navigation graph xml for each of your tabs under the res/navigation directory.
Filename: tab0.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/tab0"
app:startDestination="#id/fragmentA"
tools:ignore="UnusedNavigation">
<fragment
android:id="#+id/fragmentA"
android:label="#string/fragment_A_title"
android:name="com.app.subdomain.fragA"
>
</fragment>
</navigation>
Repeat the above template for your other tabs. Important all fragments and the navigation graph has an id (e.g. #+id/tab0, #+id/fragmentA).
Setup Bottom Navigation View
Ensure the navigation ids are the same as the ones specified on the bottom menu xml.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="#string/fragment_A_title"
android:id="#+id/tab0"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_B_title"
android:id="#+id/tab1"
android:icon="#drawable/ic_baseline_add_alert_24"/>
<item android:title="#string/fragment_C_title"
android:id="#+id/tab2"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_D_title"
android:id="#+id/tab3"
android:icon="#drawable/ic_baseline_more_horiz_24"/>
</menu>
Setup Activity Main XML
Ensure FragmentContainerView is being used and not <fragment and do not set the app:navGraph attribute. This will set later in code
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/main_toolbar"
/>
Main Activity XML
Copy over the following Code into your main activity Kotlin file and call setupBottomNavigationBar within OnCreateView. Ensure you navGraphIds use R.navigation.whatever and not R.id.whatever
private lateinit var currentNavController: LiveData<NavController>
private fun setupBottomNavigationBar() {
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val navGraphIds = listOf(R.navigation.tab0, R.navigation.tab1, R.navigation.tab2, R.navigation.tab3)
val controller = bottomNavigationView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.fragmentContainerView,
intent = intent
)
controller.observe(this, { navController ->
val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
val appBarConfiguration = AppBarConfiguration(navGraphIds.toSet())
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
setSupportActionBar(toolbar)
})
currentNavController = controller
}
override fun onSupportNavigateUp(): Boolean {
return currentNavController?.value?.navigateUp() ?: false
}
Copy NavigationExtensions.kt File
Copy the following file to your codebase
[EDIT] The above link is broken. Found it in a forked repo
Source
Google's Solution
If you are using Jetpack, the easiest way to solve this is using ViewModel
You have to save all valuable data and not make unnecessary database loads or network calls everytime you go to a fragment from another.
UI controllers such as activities and fragments are primarily intended to display UI data, react to user actions, or handle operating system communication, such as permission requests.
Here is when we use ViewModels
ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.
So if the fragment is recreated, all your data will be there instantly instead of make another call to database or network. Its important to know that if the activity or fragment that holds the ViewModel is reacreated, you will receive the same ViewModel instance created before.
But in this case you have to specify the ViewModel to have activity scope instead of fragment scope, independently if you are using a shared ViewModel for all the fragments, or a different ViewModel for every fragment.
Here is a little example using LiveData too:
//Using KTX
val model by activityViewModels<MyViewModel>()
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
//Not using KTX
val model by lazy {ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]}
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
And that's it! Google is actively working on multiple back stack support for bottom tab Navigation and claim that it'll arrive on Navigation 2.4.0 as said here and on this issue tracker if you want and/or your problem is more related to multiple back stack, you can check out those links
Remember fragments still be recreated, usually you don't change component behavior, instead, you adapt your data to them!
I leave you some useful links:
ViewModel Overview Android Developers
How to communicate between fragments and activities with ViewModels - on Medium
Restoring UI State using ViewModels - on Medium
Quick tip, if you just want to prevent loading the already selected fragment just override setOnNavigationItemReselectedListener and do nothing, but this won't save the fragment states
binding.navBar.setOnNavigationItemReselectedListener { }
You use the old version, you just use version 2.4.0-alpha05 or above.
This answer is updated in 2021.
androidx.navigation:navigation-runtime-ktx:2.4.0-alpha05
androidx.navigation:navigation-fragment-ktx:2.4.0-alpha05
androidx.navigation:navigation-ui-ktx:2.4.0-alpha05
If you use NavigationUI.setupWithNavController(), the NavOptions are defined for you with NavigationUI.onNavDestinationSelected(). These options include launchSingleTop and, if the menu item is not secondary, a popUpTo the root of the graph.
The problem is, that launchSingleTop still replaces the top fragment with a new one. To resolve this issue, you'd have to create your own setupWithNavController() and onNavDestinationSelected() functions. In onNavDestinationSelected() you'd just adapt the NavOptions to your needs.
If you are using navigation component,In addition to this answer From version:'2.4.0-alpha01' it has inbuilt support for multiple back stacks.So no navigation extension is needed
refer to this link for more details. https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
Try something like this
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
And
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> {
fragmentManager.beginTransaction().hide(active).show(homeFragment).commit()
active = homeFragment
return#OnNavigationItemSelectedListener true
}
R.id.news -> {
fragmentManager.beginTransaction().hide(active).show(newsFragment).commit()
active = newsFragment
return#OnNavigationItemSelectedListener true
}
R.id.markets -> {
fragmentManager.beginTransaction().hide(active).show(marketsFragment).commit()
active = marketsFragment
return#OnNavigationItemSelectedListener true
}
R.id.explore -> {
fragmentManager.beginTransaction().hide(active).show(exploreFragment).commit()
active = exploreFragment
return#OnNavigationItemSelectedListener true
}
}
false
}
create a class:
#Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml
class KeepStateNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
var initialNavigate = false
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
} else {
initialNavigate = true
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
val className = destination.className
fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commitNow()
return if (initialNavigate) {
destination
} else {
null
}
}
}
Use keep_state_fragment instead of fragment in nav_graph
In Activity:
val navController = findNavController(R.id.nav_host_fragment)
// get fragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
// setup custom navigator
val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
navController.navigatorProvider += navigator
// set navigation graph
navController.setGraph(R.navigation.nav_graph)
bottom_navigation.setupWithNavController(navController)
Use this snippet:
private fun attachFragment(fragmentTag: String) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
supportFragmentManager.findFragmentByTag(fragmentTag)?.let {
if (supportFragmentManager.backStackEntryCount == 0) return
val currentFragmentTag = supportFragmentManager.getBackStackEntryAt(supportFragmentManager.backStackEntryCount - 1).name
(supportFragmentManager.findFragmentByTag(currentFragmentTag) as? FragmentBase)?.let { curFrag ->
fragmentTransaction.hide(curFrag)
}
fragmentTransaction.show(it)
} ?: run {
when (fragmentTag) {
FragmentHome.TAG -> FragmentBase.newInstance<FragmentHome>()
FragmentAccount.TAG -> FragmentBase.newInstance<FragmentAccount>()
else -> null
}?.let {
fragmentTransaction.add(R.id.container, it, fragmentTag)
fragmentTransaction.addToBackStack(fragmentTag)
}
}
fragmentTransaction.commit()
}
You can use this pass the tag of specific fragment that you want to show now, using method attachFragment(FragmentHome.TAG)
Hi friend, it's new solution:
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
if (item.getItemId() != binding.navView.getSelectedItemId())
NavigationUI.onNavDestinationSelected(item, navController);
return true;
}
});
I was looking for the best way to handle this and finally i came out with this simple idea : deactivate the MenuItem currently selected.
This way, you cannot click twice on it and therefore reloading the fragment is prevented.
Don't forget to Enable it back when you go to another fragment through your navHost.
The mechanic is based on the NavHostFragment which receives your BottomNavigationView from within a fragment/activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navHostFragment =
childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navView: BottomNavigationView = view.findViewById(R.id.nav_view)
val navController = navHostFragment.navController
//Here you link the NavHostFragment's navController to your
//bottomMenu
navView.setupWithNavController(navController)
//Add a listener monitoring the destination changes
navController.addOnDestinationChangedListener(object : NavController.OnDestinationChangedListener{
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
/* Disable the selected item and re-enable the others */
for( item in navView.menu.iterator()){
item.isEnabled = item.itemId != navView.selectedItemId
}
}
})
}
Hoping it might help
Lenzy
Try this: For ChipNavigationBar
private void Bottom_navigation() {
final Fragment fragment1 = new home_fragment();
final Fragment fragment2 = new bottom_nav1_Bookmark_Fragment();
final Fragment fragment3 = new bottom_nav1_Search_Fragment();
final FragmentManager fm = getSupportFragmentManager();
ChipNavigationBar chipNavigationBar = findViewById(R.id.chipNavigation);
chipNavigationBar.setItemSelected(R.id.home, true);
fm.beginTransaction().add(R.id.frame, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.frame, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.frame,fragment1, "1").commit();
chipNavigationBar.setOnItemSelectedListener(new ChipNavigationBar.OnItemSelectedListener() {
#Override
public void onItemSelected(int i) {
Fragment active = fragment1;
Fragment fragment = null;
switch (i) {
case R.id.home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
chipNavigationBar.animate().translationY(0);
break;
case R.id.tajbi:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
break;
case R.id.more_App:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
break;
}
}
});
}
You can resolve this issue by using this solution.
First, declare fragments that are used in the bottom navigation view.
val fragment1: Fragment = HomeFragment()
val fragment2: Fragment = ProfileFragment()
val fragment3: Fragment = SettingsFragment()
val fm: FragmentManager = supportFragmentManager
var active = fragment1
Now you need to setup the bottom navigation like this
private fun setUpBottomNavigation() {
fm.beginTransaction().add(R.id.mainHostFragment, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.mainHostFragment, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.mainHostFragment,fragment1, "1").commit();
val navigation = findViewById<View>(R.id.bottomNavigationView) as BottomNavigationView
navigation.setOnItemSelectedListener(mOnNavigationItemSelectedListener)
}
private val mOnNavigationItemSelectedListener =
NavigationBarView.OnItemSelectedListener { item ->
when (item.itemId) {
R.id.homeFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment1).commit()
active = fragment1
}
R.id.assessmentListFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment2).commit()
active = fragment2
}
R.id.settingsFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment3).commit()
active = fragment3
}
}
false
}

Android navigation component: how save fragment state

I use bottomNavigationView and navigation component. Please tell me how I don't destroy the fragment after switching to another tab and return to the old one? For example I have three tabs - A, B, C. My start tab is A. After I navigate to B, then return A. When I return to tab A, I do not want it to be re-created. How do it? Thanks
As per the open issue, Navigation does not directly support multiple back stacks - i.e., saving the state of stack B when you go back to B from A or C since Fragments do not support multiple back stacks.
As per this comment:
The NavigationAdvancedSample is now available at https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
This sample uses multiple NavHostFragments, one for each bottom navigation tab, to work around the current limitations of the Fragment API in supporting multiple back stacks.
We'll be proceeding with the Fragment API to support multiple back stacks and the Navigation API to plug into it once created, which will remove the need for anything like the NavigationExtensions.kt file. We'll continue to use this issue to track that work.
Therefore you can use the NavigationAdvancedSample approach in your app right now and star the issue so that you get updates for when the underlying issue is resolved and direct support is added to Navigation.
In case you can deal with destroying fragment, but want to save ViewModel, you can scope it into the Navigation Graph:
private val viewModel: FavouritesViewModel by
navGraphViewModels(R.id.mobile_navigation) {
viewModelFactory
}
Read more here
EDIT
As #SpiralDev noted, using Hilt simplifies a bit:
private val viewModel: MainViewModel by
navGraphViewModels(R.id.mobile_navigation) {
defaultViewModelProviderFactory
}
just use navigation component version 2.4.0-alpha01 or above
Update:
Using last version of fragment navigation component, handle fragment states itself. see this sample
Old:
class BaseViewModel : ViewModel() {
val bundleFromFragment = MutableLiveData<Bundle>()
}
class HomeViewModel : BaseViewModel () {
... HomeViewModel logic
}
inside home fragment (tab of bottom navigation)
private var viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.bundleFromFragment.observe(viewLifecycleOwner, Observer {
val message = it.getString("ARGUMENT_MESSAGE", "")
binding.edtName.text = message
})
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.bundleFromFragment.value = bundleOf(
"ARGUMENT_MESSAGE" to binding.edtName.text.toString(),
"SCROLL_POSITION" to binding.scrollable.scrollY
)
}
You can do this pattern for all fragments inside bottom navigation
Update 2021
use version 2.4.0-alpha05 or above.
don't use this answer or other etc.
This can be achieved using Fragment show/hide logic.
private val bottomFragmentMap = hashMapOf<Int, Fragment>()
bottomFragmentMap[0] = FragmentA.newInstance()
bottomFragmentMap[1] = FragmentB.newInstance()
bottomFragmentMap[2] = FragmentC.newInstance()
bottomFragmentMap[3] = FragmentD.newInstance()
private fun loadFragment(fragmentIndex: Int) {
val fragmentTransaction = childFragmentManager.beginTransaction()
val bottomFragment = bottomFragmentMap[fragmentIndex]!!
// first time case. Add to container
if (!bottomFragment.isAdded) {
fragmentTransaction.add(R.id.container, bottomFragment)
}
// hide remaining fragments
for ((key, value) in bottomFragmentMap) {
if (key == fragmentIndex) {
fragmentTransaction.show(value)
} else if (value.isVisible) {
fragmentTransaction.hide(value)
}
}
fragmentTransaction.commit()
}
Declare fragment on the activity & create fragment instance on onCreate method, then pass the fragment instance in updateFragment method. Create as many fragment instances as required corresponding to bottom navigation listener item id.
Fragment fragmentA;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
fragmentA = new Fragment();
updateFragment(fragmentA);
}
public void updateFragment(Fragment fragment) {
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.layoutFragment, fragment);
transaction.commit();
}
Furthermore be sure you are using android.support.v4.app.Fragment and calling getSupportFragmentManager()

Categories

Resources