Creating a reusable actions menu in Jetpack Compose
Overview
In this article we will learn how to implement a reusable composable which we can add to the top app bar in our app’s screens to show relevant menu items. The final result will look as shown below:
Creating the menu
First we will see how we can implement this menu using the existing tools in Jetpack Compose, and later we will refactor it to make it reusable, so that we do not have to keep duplicating the same code each time we need to add an actions menu to a new screen.
On Jetpack Compose most root screens will use a Scaffold, which is a slot composable that accepts, among other items, a Top Bar composable and a content composable. For the Top Bar we would typically use the TopAppBar composable from Material3, which itself is a slot composable, accepting a Title composable, a Navigation Icon composable and an Actions composable. The Actions composable is where we will place our menu action items.
Skeleton app
Let’s start by creating the skeleton for our app with a simple Top Bar, initially without any action items:
This displays our app bar and a text for content:
Adding the action menu
Next we will add the actions menu. The TopAppBar from Material3, as mentioned, has a slot for the actions menu, which is a composable on a RowScope, so that the items we add here will display in a row, right aligned.
For our sample case, we want to display 2 items always visible, and we want to have other items available via an overflow button, so we need to add 4 items here, the 2 action items that are always visible, the overflow item, and finally the drop down menu.
Let’s see the code that we need in order to have these items displayed as an action menu and then we’ll walk it to explain how it works:
We have to keep track of the menu state, whether it’s open or not, so we use a mutableState variable for that.
We provide a composable for the actions slot in the TopAppBar component.
The actions content composable has a RowScope as receiver, so we place the action menu items in the order we want them displayed, first the 2 items that we want visible, using an IconButton for each of them.
Next we add the overflow item, similar to the 2 visible items. The onClick for this item will toggle the visibility of the overflow menu, so we toggle the value of menuExpanded here.
The overflow menu is implemented using a DropdownMenu, and we use our menuExpanded flag to indicate whether it should be open or not. Likewise, when the user dismisses the drop down menu, we set the flag to false.
Within the DropdownMenu we add DropdownMenuItems for each action item we want displayed.
So, as we can see, it is not complex to add an actions menu to the TopAppBar, but if we have multiple screens where we need to do this it can become quite tedious, so let’s refactor this so that we can create an actions menu without all this boilerplate.
Creating the reusable actions menu component
What we need
We will model our actions menu component on the XML menus from the view system. Below we can see a typical menu for an app:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:title="@string/menu_search"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/favorites"
android:icon="@drawable/ic_favorite_border"
android:title="@string/menu_favorites"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/menu_refresh"
app:showAsAction="ifRoom" />
<item
android:id="@+id/settings"
android:title="@string/menu_settings"
app:showAsAction="never" />
<item
android:id="@+id/about"
android:title="@string/menu_about"
app:showAsAction="never" />
</menu>
We can see that, for each action item, we will need:
a title
the display mode (always shown, shown if room, never shown)
an icon, for items always shown or those shown if room is available
Not shown above, but we will also need the following, as we are using an IconButton with an Icon to represent the action items:
a click handler
a content description for the item
Defining the action items
We can see from above that we have 3 different display modes, with items sharing a set of properties (title, click listener) regardless of display mode, so this would be a good candidate to represent as a sealed class, so let’s do just that, defining a hierarchy of sealed classes to represent our action items:
We define our sealed class for the action items. Because we do not have any state in the base class, we actually define it as an interface.
The interface defines the common properties for all action items, a title and a click handler.
Next we define the concrete classes, starting with the AlwaysShown which adds the contentDescription and the icon to the base class.
We do the same with the ShownIfRoom class.
And finally we define the NeverShown class, which does not have any additional properties and simply implements those from the interface.
We can see above that the AlwaysShown and ShownIfRoom classes have the same properties, so we can aggregate these under a new sealed interface that will define the 2 new properties, the contentDescription and the icon. Let’s do that and reorganize our hierarchy of action item classes:
We define a new sealed interface, IconMenuItem, inheriting from ActionMenuItem that adds the properties common to both AlwaysShown and ShownIfRoom.
The AlwaysShown and ShownIfRoom classes now inherit from the new interface, IconMenuItem.
Now that we have a way to represent the action menu items, let’s create our menu composable to display them.
Creating the actions menu composable
The first thing we have to figure out is how we will display the action menu items. We have items that will always be shown in the TopAppBar, others that will never be shown there (only shown in the drop down menu), and a 3rd category of items that might or might not be shown in the TopAppBar.
To determine whether an item of type ShownIfRoom will be shown or not we have to set an upper limit on the number of action items we are willing to display in the TopAppBar. Once we have agreed on that upper limit, we need to count all the items that are always shown and, if that number is below our upper limit, we can then move items of type ShownIfRoom to be in the TopAppBar instead of in the overflow menu. However, we need to also take into account that, if we have overflow items, then we need to display the overflow action item in the TopAppBar, taking away one of the available slots. Our logic then needs to be as follows:
Count the number of items that are always shown in the TopAppBar.
Determine if we have overflow items to show in the drop down menu.
Determine the number of available slots in the TopAppBar, based on the always displayed items and whether we have an overflow action item.
If we have available slots, promote items of type ShownIfRoom to the TopAppBar.
Move any remaining ShownIfRoom items to the drop down menu.
Let’s create a method that will receive a list of ActionMenuItems, and will split them into 2 buckets, one for items that are always shown in the TopAppBar and another for items that are relegated to the drop down menu:
First we define a data class to hold the result, we have 2 sets of action items, those that are shown in the TopAppBar and those that are in the overflow drop down menu. Note that the list for the always shown items is of type ActionMenuItem.IconMenuItem because we can accept AlwaysShown or ShownIfRoom items here.
Our method to split the items receives a list of ActionMenuItems, the interface representing our action menu items, and the number of items we want to show in the TopAppBar.
Next we split the items into 3 buckets, leveraging their types, one of the advantages of using sealed classes to represent the action items. We convert the lists for AlwaysShown and ShownIfRoom items to mutable lists because we may need to promote some ShownIfRoom items to AlwaysShown items.
Next we determine if we will have overflow items — we do if we have items of type NeverShown (as these always go to the drop down), or if the number of AlwaysShown items plus the number of ShownIfRoom items exceeds the maximum number of visible items, minus 1, to account for the overflow action item. Basically, what we try to do here is promote all ShownIfRoom items to be AlwaysShown items and see whether their fit or not.
Now we calculate how many slots have been used up, that’s the number of AlwaysShown items, plus 1 if we have overflow items.
Next we calculate how many available slots we have in the TopAppBar — that’s the maximum allowed, minus the AlwaysShown items, minus 1 if we have to show the overflow action menu.
Here we check if we have available slots, and whether we have items of type ShownIfRoom that we can promote to the AlwaysShown type.
If that’s the case, we get a sublist from the ShownIfRoom list for the number of available slots we have, we add those to the AlwaysShown list, and remove them from the ShownIfRoom list. In other words, we promote as many items from the ShownIfRoom list to the AlwaysShown list as available slots we have.
Finally we have our result, the items that will display in the TopAppBar are those that were of type AlwaysShown plus the items of type ShownIfRoom that were promoted, and the overflow items are the reminder of the ShownIfRoom items, plus all of the NeverShown items that were always supposed to be in the drop down menu.
Now that we have split our actions menu items into the 2 buckets, it’s just a matter of displaying them in a composable, so let’s see how we can do that:
We define our ActionsMenu composable accepting a list of ActionMenuItem, a flag that indicates if the drop down menu is open, a callback for when the user taps on the overflow menu icon, and the number of visible items we want to show in the TopAppBar. Note that we make our composble stateless by hosting the flag that determines whether the drop down menu is open.
Next we split our action menu items into the 2 buckets we described earlier. We use a remember here so that we do not keep doing this at each recomposition, the buckets are good as long as the items and the maxVisibleItems do not change, so we use those 2 attributes as keys on the remember method.
After this we iterate over the AlwaysShown items and add them to the TopAppBar, using the icon and content description for each of them.
Next we check if we have overflow items for the drop down menu.
If that’s the case, we add the overflow action menu item, and we use the onToggleOverflow method as the click handler.
The next step is to add the DropdownMenu composable to host the drop down items. We pass the isOpen flag and the callback to toggle the menu.
Finally, for each item in the overflow we add a DropdownMenuItem using the title and the click handler.
And with this our reusable action items menu composable is complete, we can use it as shown below:
We define a flag to persist the state of the overflow menu — as we did in the original implementation.
We add our ActionsMenu to the actions slot on the TopAppBar.
We provide a list of action items, some of type AlwaysShown, others of type ShownIfRoom, and finally others of type NeverShown which are always relegated to the overflow menu.
Here we provide the current drop down state, open or closed, for the overflow menu.
And finally this is the callback to toggle the state of the drop down menu.
This concludes this article, a full implementation of the actions menu is available in this gist.