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?
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 ofFragmentActivity
andAppCompatActivity
] now implements theMenuHost
interface. This allows any component to add menu items to theActionBar
by adding aMenuProvider
instance to the activity. EachMenuProvider
can optionally be added with a Lifecycle that will automatically control the visibility of those menu items based on theLifecycle
state and handle the removal of theMenuProvider
when theLifecycle
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:
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.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).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).