Search code examples
androidkotlinandroid-fragmentsandroid-toolbarandroid-menu

Android MenuProvider does not invoke onMenuItemSelected() when a MenuItem is selected


On my working project we need to use SearchView in a few Fragments. The search bar itself have to be placed in Toolbar and opened by clicking the search button at the end of the Toolbar (see the image). So, I implemented such behavior by:

  • adding the menu.xml menu file containing the search item only;
  • adding the layout_search.xml layout file containing only the androidx.appcompat.widget.SearchView as root view;
  • implementing the MenuProvider interface in my fragment.

Then, when I tried to test whether it works as expected I found that everything written inside onMenuItemSelected() method is never being invoked. I mean when search button in toolbar is clicked, the search bar appears inside toolbar just as expected, but the system does not call my listener that handles the menu item selection.

I just do not understand why menu item selection callbacks are not invoked. I tried one more way to handle toolbar's menu and its items selection (e.g. placing the toolbar in my fragment, inflating the menu and then setting binding.toolbar.setOnMenuItemClickListener with needed handler), but it has also led to the same ending - item selection callbacks just do not work at all.

I hope there is anyone who have ever faced the same problem and found a solution, because I have already wasted 3 days trying to get item selection callbacks working and now I feel stumped.

To reproduce the problem, I created a new project with bottom navigation bar template, added resources and implemented the MenuProvider interface in one of the automatically generated fragments. To test whether the item selection callbacks are being invoked I used logger and none of my log messages appeared in logcat. Code examples are below:

HomeFragment.kt

class HomeFragment : Fragment(), MenuProvider {

    private var _binding: FragmentHomeBinding? = null


    private val binding get() = _binding!!
    private val queryConsumer: (String) -> Unit = { Log.i("HOME_FRAGMENT", "CONSUMED QUERY: $it") }
    private var searchView: SearchView? = null
    private val onBackPressed: OnBackPressedCallback = object : OnBackPressedCallback(false) {
        override fun handleOnBackPressed() {
            clearSearch()
            closeSearch()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        requireActivity().onBackPressedDispatcher.addCallback(onBackPressed)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val homeViewModel =
            ViewModelProvider(this)[HomeViewModel::class.java]

        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val textView: TextView = binding.textHome
        homeViewModel.text.observe(viewLifecycleOwner) {
            textView.text = it
        }
        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val menuHost = requireActivity() as MenuHost
        menuHost.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.search_menu, menu)
    }

    override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
        R.id.search_menu_item -> {
            Log.i("HOME_FRAGMENT", "NOW IN SEARCH MENU ITEM BRANCH")

            searchView = menuItem.actionView as? SearchView

            searchView?.run {
                queryHint = "hint"
                maxWidth = Int.MAX_VALUE

                setOnClickListener {
                    onSearchOpened()
                }

                setOnCloseListener {
                    onSearchClosed()

                    queryConsumer.invoke("")

                    return@setOnCloseListener false
                }

                setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                    override fun onQueryTextSubmit(query: String): Boolean {
                        queryConsumer.invoke(query)
                        return true
                    }

                    override fun onQueryTextChange(newText: String): Boolean {
                        queryConsumer.invoke(newText)
                        return true
                    }
                })
            }

            true
        }
        else -> {
            Log.i("HOME_FRAGMENT", "NOW IN ELSE BRANCH")
            false
        }
    }

    fun clearSearch() {
        searchView?.setQuery("", true)
    }

    fun closeSearch() {
        searchView?.isIconified = true
    }

    fun onSearchClosed() {
        onBackPressed.isEnabled = false
    }

    fun onSearchOpened() {
        onBackPressed.isEnabled = true
    }
}

layout_search.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.SearchView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/search"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

</androidx.appcompat.widget.SearchView>

search_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
>
    <item
        android:id="@+id/search_menu_item"
        android:contentDescription="@string/search_menu_title"
        android:icon="?android:attr/actionModeWebSearchDrawable"
        android:title="@string/search_menu_title"
        app:actionViewClass="androidx.appcompat.widget.SearchView"
        app:showAsAction="always" />
</menu>

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navView: BottomNavigationView = binding.navView

        val navController = findNavController(R.id.nav_host_fragment_activity_main)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)


    }
}

Solution

  • The problem was hiding in the menu .xml file... I just randomly (from despair) changed the attribute app:showAsAction value from always to ifRoom|collapseActionView and all callbacks (both onMenuItemSelected from MenuProvider and setOnMenuItemClickListener from toolbar) started to work!