androidandroid-fragmentsandroid-menu

Hiding menu items in Fragment and showing them again on navback after `setHasOptionsMenu` deprecation with new menu provider API


A month or so ago, the Android team deprecated onCreateOptionsMenu and onOptionsItemSelected, as well as setHasOptionsItemMenu. This unfortunately broke all of my code.

My app has a lot of fragments, and when the user navigates to them, I always made sure that the menu items would disappear and reappear on navigating back, with the following code:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setHasOptionsMenu(true)
}
override fun onPrepareOptionsMenu(menu: Menu) {
    super.onPrepareOptionsMenu(menu)
    menu.clear()
}

This code worked well and was really simple. Now that the Android team has deprecated (why?) setHasOptionsMenu, I cannot recreate this code.

I understand the new syntax for inflating menu items and handling menu item click events, although I cannot figure out -- for the life of me -- how to hide the menu in a fragment and then show it again on navigation back using the new menu provider API.

Here's what I've tried:

Navigating to the fragment:

if (supportFragmentManager.backStackEntryCount == 0) {
            supportFragmentManager.commit {
                replace(R.id.activityMain_primaryFragmentHost, NewProjectFragment.newInstance(mainSpotlight != null))
                addToBackStack(null)
            }
        }

getRootMenuProvider function in ActivityFragment interface:

interface ActivityFragment {
    val title: String

    companion object {
        fun getRootMenuProvider() = object : MenuProvider {
            override fun onPrepareMenu(menu: Menu) {
                for (_menuItem in menu.children) {
                    _menuItem.isVisible = false
                }
            }

            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
            }

            override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                return false
            }
        }
    }
}

Using the getRootMenuProvider function:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val menuHost: MenuHost = requireActivity()
        menuHost.addMenuProvider(ActivityFragment.getRootMenuProvider())
    }

MainActivity (trying to restore the menu items to their previous state):

    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
        for (_menu in menu.children) {
            _menu.isVisible = true
        }

        return super.onPrepareOptionsMenu(menu)
    }

    override fun onBackPressed() {
        super.onBackPressed()
        findViewById<BottomNavigationView>(R.id.activityMain_bottomNavigationView)?.visibility = View.VISIBLE
        invalidateOptionsMenu()
    }

This hides the items in the fragment, but the items still remain hidden after navigating back until the user reloads the activity by rotating their screen, or doing something similar.

How to hide the menu items in a fragment and reappear them on navigation back with the new menu provider API?


Solution

  • Short term

    The reason everything 'broke' is because you are assuming that menu.clear() and the dispatch of fragment menu calls happen after your activity has added its own menu items. Fragments now go through the dispatch of menu calls when your activity calls super.onCreateOptionsMenu() or super.onPrepareOptionsMenu() so often you can 'fix' your problem by making that the last thing your override calls, rather than the first.

    Long term

    In fact, you are doing a lot wrong: the global menu controlled by your Activity is a shared resource and no individual fragment should ever, ever be manually clearing the entire menu. This breaks activity menu items, child fragment menu items, as well as other fragment's menu items. Only the component that inflated certain menu items should ever be touching those specific menu items.

    So to fix your problem, you should follow the Activity 1.4.0-alpha01 release notes (the release that added the MenuHost and MenuProvider integration into the Activity layer:

    AndroidX ComponentActivity [and its subclasses of FragmentActivity and AppCompatActivity] now implements the MenuHost interface. This allows any component to add menu items to the ActionBar by adding a MenuProvider instance to the activity. Each MenuProvider can optionally be added with a Lifecycle that will automatically control the visibility of those menu items based on the Lifecycle state and handle the removal of the MenuProvider when the Lifecycle is destroyed.

    They go onto show an example of its usage in a Fragment:

    /**
      * Using the addMenuProvider() API in a Fragment
      **/
    ExampleFragment : Fragment(R.layout.fragment_example) {
    
      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // The usage of an interface lets you inject your own implementation
        val menuHost: MenuHost = requireActivity()
      
        // Add menu items without using the Fragment Menu APIs
        // Note how we can tie the MenuProvider to the viewLifecycleOwner
        // and an optional Lifecycle.State (here, RESUMED) to indicate when
        // the menu should be visible
        menuHost.addMenuProvider(object : MenuProvider {
          override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
            // Add menu items here
            menuInflater.inflate(R.menu.example_menu, menu)
          }
    
          override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
            // Handle the menu selection
            return true
          }
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)
      }
    

    This shows off three things in particular:

    1. A single MenuProvider should only be touching its Menu Items. You should never, ever, ever be "clearing all menu items" or anything that affects another component's menu items.
    2. By calling addMenuProvider with a Lifecycle (in this case, the Fragment view's Lifecycle - i.e., the one that only exists when the Fragment's view is on screen), then you automatically hide the menu items when your Fragment's view is destroyed (when your replace call happens) and automatically reshown when your fragment's view re-appears (i.e., when the back stack is popped).
    3. That the fragment itself that is controlling the Lifecycle and visibility of the menu items should be the one creating and handling its own menu items. Your activity (which can add its own MenuProvider as seen in the other example) should only be adding menu items that exist for the entire Lifecycle of the activity (items that are visible on all fragments).