Cannot go back to Main Fragment after going into SettingsFragment using ActionBar

Hope all are safe. I have a frustrating issue using the ActionBar and the Up Button within the ActionBar. I have the following fragment structure in my app:

MainFragment -
             |- SettingsFragment -
                                 |- GeneralSettingsFragment
                                 |- ScaryStuffSettingsFragment

All of the Fragments above have the ActionBar enabled. SettingsFragment, GeneralSettingsFragment and ScaryStuffFragment all have the Up Button enabled so that the user should be able to go back if needed, in the order above.

In my MainFragment, I have the following code to get to to the SettingsFragment:

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        super.onOptionsItemSelected(item)

        when(item.itemId) {
            R.id.action_main_to_settings_menu -> {
                onOptionSettingsClick()
            }

        }
        return true
    }

    /** When the user taps the options menu settings button, we'll take them to the
     * settings menu.
     */
    private fun onOptionSettingsClick() {
        findNavController().navigate(R.id.settingsFragment)
    }

In my SettingsFragment I have the following:

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            android.R.id.home -> findNavController().navigateUp()
        }
        return super.onOptionsItemSelected(item)
    }

The code that is used in SettingsFragment is identical to the code used in ScaryStuffSettingsFragment and GeneralSettingsFragment.

Now when I tap on Settings in MainFragment, it takes me to the SettingsFragment. No problems there. If I tap the Up Button in the SettingsFragment ActionBar, same again, it sends me to MainFragment.

If I tap either Scary Stuff or General Settings, it takes me to the relevant fragments no problem. If I tap the Up Button in either Fragments, it takes me to SettingsFragment. Again. No problems here.

However, if I then want to go back to MainFragment from SettingsFragment once I have tapped either Scary Stuff or General using the Up Button... Nothing happens.

What I have found is that if I tap the back button on the device itself, it then takes me back to the MainFragment circumventing the problem entirely but I don't want the user to use the Up Button for most of the settings and then have to tap the back button (if that makes sense).

What is happening here for it to "forget" the transfer between MainFragment and SettingsFragment once one of the sub fragments are selected?

Thanks!

EDIT:

MainFragment is a normal fragment. It's onCreateView is as follows:

override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstance: Bundle?): View {
        _binding = FragmentMainBinding.inflate(inflater, container, false)
        setHasOptionsMenu(true)

        return binding.root
    }

All three of my Settings apps are PreferenceFragments so have the following:

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.general_settings_preferences, rootKey)
        setHasOptionsMenu(true)
    }

EDIT 2:

I now have the following:

MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    navController = findNavController(R.id.nav_host)

        appBarConfiguration = AppBarConfiguration.Builder(R.id.FragmentMain).build()
        val hostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_container) as NavHostFragment?
        //navController = hostFragment?.navController!! // Commented out due to throwing a nullpointerexception
        setupActionBarWithNavController(navController, appBarConfiguration)
}

override fun onSupportNavigateUp(): Boolean {
        return (NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp())
}

When I just do this (and update my nav graph too), and then tap Settings (Which goes to my SettingsFragment) it briefly shows SettingsFragment then changes to Settings on the top (this is because I tell it to with (activity as AppCompatActivity).supportActionBar?.title = getString(R.string.menu_option_settings) in my onCreateOptionsMenu method. Then, when I tap back, this works as expected. However, when I then tap either Scary Stuff (to go to ScaryStuffSettingsFragment) or General Settings (to go to GeneralSettingsFragment) it does the same thing and flashes the name of the Fragment in the top, then changes to what I've set it and when I tap the up arrow, it sends me back to the Main Fragment.

I only want the Settings menu to go back to the MainFragment, the other settings should go to the Settings Menu (if that makes sense).


Solution 1:

if I then want to go back to MainFragment from SettingsFragment once I have tapped either Scary Stuff or General using the Up Button... Nothing happens.

Nothing happens means that the navigation components didn't manage the UP button clicks; and apparently you manually manage the back stack navigation as per:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when(item.itemId) {
        android.R.id.home -> findNavController().navigateUp()
    }
    return super.onOptionsItemSelected(item)
}

In order to fix this, make sure to add enable supportActionBar navigation in the activity that hosts these fragments (documentation reference):

  • Call setupActionBarWithNavController()
  • Override onSupportNavigateUp()
class MainActivity : AppCompatActivity() {

    lateinit var navController: NavController

    private lateinit var mAppBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mAppBarConfiguration = AppBarConfiguration.Builder(
            R.id.main_fragment // Adjust that to your top level fragments (that you don't want the UP button appear)
        ).build()

        val hostFragment =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment1) as NavHostFragment?
        navController = hostFragment?.navController!!
        setupActionBarWithNavController(this, navController)

    }

    override fun onSupportNavigateUp(): Boolean {
        return (NavigationUI.navigateUp(navController, mAppBarConfiguration)
                || super.onSupportNavigateUp())
    }

}

Note: if you are not using the default supportActionBar, to enable the navigation components controls your custom toolBar:

toolbar.setupWithNavController(navController, appBarConfiguration)

So far this makes the actionBar & its navigation controlled by navigation components/navController. And the navigation through the UP button will be registered into the backstack normally.

But to remove some fragments from the back stack on any navigation actions you can use popUpTo & popUpToInclusive. This answer describes the difference with an example.

Applying that to your case; you do this through the action from SettingsFragment to GeneralSettingsFragment|ScaryStuffSettingsFragment

Either in the navGraph:

<navigation ...>

    ....
    
    <fragment
        android:id="@+id/fragmentSettings"
        android:name="com.example.android.ktnavarchcomptwoactivities.SettingsFragment"
        android:label="fragmentSettings" >
        <action
            android:id="@+id/action_fragmentSettings_to_fragmentScaryStuffSettings"
            app:destination="@id/fragmentScaryStuffSettings"         
            app:popUpTo="@id/fragment_main"
            app:popUpToInclusive="false"/>

        <action
            android:id="@+id/action_fragmentSettings_to_fragmentGeneralSettings"
            app:destination="@id/fragmentGeneralSettings" 
            app:popUpTo="@id/fragment_main"
            app:popUpToInclusive="false"/>
    </fragment>


</navigation>

Or programmatically by defining the NavOptions parameter during the navigation:

In SettingsFragment:

// Navigation from SettingsFragment to GeneralSettingsFragment
findNavController().navigate(
    R.id.action_fragmentSettings_to_fragmentGeneralSettings,
    null,
    NavOptions.Builder().setPopUpTo(R.id.fragment_main, false).build()
)


// Navigation from SettingsFragment to ScaryStuffSettingsFragment
findNavController().navigate(
    R.id.action_fragmentSettings_to_fragmentScaryStuffSettings,
    null,
    NavOptions.Builder().setPopUpTo(R.id.fragment_main, false).build()
)