How to add items to the menu bar using the new MenuProvider? - android

Very much a beginner question here. Since the old paradigm which used "setHasOptionsMenu(true)" in the fragments was recently deprecated in Android Studio, I have been trying to convert my app to the newest scheme as outlined in the documentation. All explanation about this I can find centers around the following code snippet from said documentation:
/**
* Using the addMenuProvider() API directly in your Activity
**/
class ExampleActivity : ComponentActivity(R.layout.activity_example) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Add menu items without overriding methods in the Activity
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
}
})
}
}
/**
* Using the addMenuProvider() API in a Fragment
**/
class 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)
}
Now, I have tried to add this code to my main activity (extending AppCompatActivity() ) and associated fragments. Wherever it says "R.menu.example_menu" I have inserted my own menu layout files which contain basically only a single settings item most of the time.
However, while the code compiles without error, no item is actually added to the menu bar. What am I missing? Am I supposed to add the items manually where it says "add menu items here"? However, writing something such as "menu.add("Settings") doesn't seem to have an effect either.

Try the following in the Main Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
In other words, use AppCompatActivity() instead of ComponentActivity(), because AppCompatActivity() extends FragmentActivity which extends ComponentActivity().
make sure to use the androidX dependencies (import androidx.appcompat.app.AppCompatActivity)
Also, I am not sure what items you have, but it could be because of their constraints in the xml files.
For example, if you have a TextView and you used
tools:text="toolsText, you won't be able to see the text, since this is used only for Android Studio layout preview and it doesn't show text when you run the app.
I hope this can be helpful for you.

Related

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?
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:
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.
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).
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).
I have a number of fragments that don't require menu. Therefore I was clearing the menu. There now appears to be no way to satisfy that requirement.

Android Studio does not render layout

I'm currently developing an android app and I've encountered a trouble that I'm not able to resolve myself.
Introduction
I've created a new project and picked "Tabbed Activity" as a template for the project. As you may know, the project created with this template, has 2 .xml files: activity_main.xml (that contains AppBarLayout and ViewPager2) and fragment_main.xml. It also has MainActivity.kt set up to make tabs work, and ui.main package with 3 .kt files in it that are responsible for displaying tabs content e.g. "Fragment #1" etc.
What do I want to have
I need the application to have top action bar with title, logo and tabs navigation. In total, I need to have 3 different tabs (fragments) with their own layout and logic.
What did I do and what happend
So, I've customized the activity_main.xml layout, then created a new layout fragment_dashboard.xml for the one of the fragments that I want to have in the application.
I've deleted auto generated code and wrote my own. Since I'm mostly like as a beginner in android development, I've used google to learn how to bind tabs, fragments and main activity together. I found several articles that I considered suitable for me.
After I finished the code, I wanted to check how my customized action bar with tabs and the half-finished fragment_dashboard.xml layout look together.
So I tried to run the app and encoutered the problem: when app starts in the emulated phone there is just a white screen and nothing else... (before deleting the auto-generated code for tabs it run without any problems)
What I tried to do
First of all I tried to debug MainActivity.kt. I put a breakpoint at the first line of the function onCreate():
super.onCreate(savedInstanceState, persistentState)
But when I run app in debug mode, debuger does not stop at this breakpoint. Thus, I came to a conclusion: execution does not even get to the function onCreate().
So, the question is: what am I doing wrong and how can I fix it to be able to see tabs and their fragments?
Code
DashboardFragment.kt
class DashboardFragment() : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_dashboard, container, false);
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.i("DashboardFragment","onViewCreated")
super.onViewCreated(view, savedInstanceState)
}
}
ViewPagerFragmentStateAdapter.kt
class ViewPagerFragmentStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
var positionToPageName = mapOf(
0 to "Dashboard"
)
private var _pageNameToFragment = mapOf<String, Fragment>(
"Dashboard" to DashboardFragment()
)
override fun getItemCount(): Int = _pageNameToFragment.size
override fun createFragment(position: Int): Fragment {
val pageName = positionToPageName[position]
val page = _pageNameToFragment[pageName]
return page ?: DashboardFragment() as Fragment
}
}
ViewPagerFragmentStateAdapter.kt
class MainActivity : AppCompatActivity() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager: ViewPager2
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
setContentView(R.layout.activity_main)
val adapter = ViewPagerFragmentStateAdapter(this)
viewPager = findViewById(R.id.view_pager)
viewPager.adapter = adapter
tabLayout = findViewById(R.id.tabs)
TabLayoutMediator(tabLayout, viewPager) {
tab, position -> tab.text = adapter.positionToPageName[position]
}.attach()
}
}
I've tried to change some things in the MainActivity.kt. It seems that the problem was just in the definition of the onCreate() function:
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
I've created a test project with "Tabbed Activity" template again and noticed that in the new project the persistentState argument is missing. So, I just removed it from definition in my project and the project started working well.
Now the definition of the onCreate() looks like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

How to clear/reset a map from outside the fragment?

I have a simple app for testing. It has a "basic activity" that Android Studio brings by default. Also a Google maps fragment shown in the basic activity (the code is at the end of the post).
As you all know, the basic activity has a floating button like this:
The main problem is that I'm completely new to working with fragments, and I don't quite understand how they work (I am working on it) but what I want to do is that: by clicking on the floating button, the map is "restarted", reloading again and cleaning it of markers that the user has placed.
What would be the correct way to do this?
I have found several suggestions but I can't get them to work or I can't see how to implement them correctly. One of the ways I have tried is to detach and attach the map fragment, but this causes it to crash and shows no results. The code is the following, which I add in the floating button listener:
val frg : Fragment? = supportFragmentManager.findFragmentById(R.id.map);
val frgTransac = supportFragmentManager.beginTransaction();
if (frg != null) {
frgTransac.detach(frg);
frgTransac.attach(frg);
frgTransac.commit();
}
Another option is to use "googleMap.clear" but I don't know exactly how to get access to that object from the floating button listener.
I hope you can help me with this and, above all, understand how the fragments work and what I am doing wrong.
Main activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
// CLEAR MAP FROM HERE
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}
Map Fragment
class FMaps : Fragment() {
private val callback = OnMapReadyCallback { googleMap ->
val sydney = LatLng(-34.0, 151.0)
googleMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_f_maps, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
mapFragment?.getMapAsync(callback)
}
}
You can use custom interface defined in the fragment and then called from the activity when the button is pressed (example how to create interface here: Communicating between a fragment and an activity - best practices). When you press the button and the interface is called, you can just refresh the map using mapFragment?.getMapAsync(callback) inside the fragment.

LiveData observer vs onPrepareOptionsMenu race

I'm working on a project which lets users in either as guests or registerd users.
There is an application scope user object with LiveData of the current user type
private val _isGuest = MutableLiveData<Boolean>()
val isGuest: LiveData<Boolean>
get() = _isGuest
There is HomeFragment which needs to show logout menu item for registered users.
The fragment has a ViewModel bound to the global property
val isGuest: LiveData<Boolean> = MainApplication.user.isGuest
and the fragment observes the data
var menu: Menu? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewModel.isGuest.observe(viewLifecycleOwner, Observer {
menu?.findItem(R.id.action_logout)?.isVisible = !it
})
}
override fun onPrepareOptionsMenu(menu: Menu) {
this.menu = menu
menu.findItem(R.id.action_logout)?.isVisible = !isGuest
super.onPrepareOptionsMenu(menu)
}
I need to toggle the menu item in the observer because registered users can logout at runtime and the current screen will need to be updated respectively.
The problem is that I also have to duplicate the code in onPrepareOptionsMenu because the observer may get notified before menu is initilized at startup.
Definitely I can move that line of code into a separate function and call it from the two points but aren't there a better solution?
Use invalidateOptionsMenu() to trigger onPrepareOptionMenu()
var menu: Menu? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewModel.isGuest.observe(viewLifecycleOwner, Observer {
activity?.invalidateOptionsMenu()//This will trigger onPrepareOptionsMenu
})
}
override fun onPrepareOptionsMenu(menu: Menu) {
this.menu = menu
menu.findItem(R.id.action_logout)?.isVisible = !isGuest
super.onPrepareOptionsMenu(menu)
}

OptionsMenu in nested fragments with Nav component

I'm using Navigation Architecture Component
and nested fragments (only 1 activity in my app and shared ToolBar). In fragment A I do:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.scan_menu, menu)
}
Then I show a new fragment B using something like this:
findNavController().navigate(R.id.action_a_b)
The navigation part of it works fine, but the menu created in fragment A sticks around when fragment b is shown (actually, it is never cleared). Isn't this supported in the nav arch components? How am I supposed to attack this? I do not wan't to perform hacks by clearing the menu manually in literally all other fragment due to one of them adding a menu.
Thanks!
Since there doesn't seem to be any solution, I ended up with keeping a reference to the MenuItem, and hide/show:
private var menuItem: MenuItem? = null
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.scan_menu, menu)
menuItem = menu.findItem(R.id.scan_menu_manual)
}
override fun onResume() {
super.onResume()
menuItem?.isVisible = true
}
override fun onPause() {
super.onPause()
menuItem?.isVisible = false
}
That seems to work for now.

Categories

Resources