Creating a reusable actions menu in Jetpack Compose
Overview
In this story 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 theTopAppBar
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 anIconButton
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 ofmenuExpanded
here. - The overflow menu is implemented using a
DropdownMenu
, and we use ourmenuExpanded
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 addDropdownMenuItem
s 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 aninterface.
- 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 thecontentDescription
and theicon
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 fromActionMenuItem
that adds the properties common to bothAlwaysShown
andShownIfRoom
. - The
AlwaysShown
andShownIfRoom
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 theTopAppBar
. - Move any remaining
ShownIfRoom
items to the drop down menu.
Let’s create a method that will receive a list of ActionMenuItem
s, 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 typeActionMenuItem.IconMenuItem
because we can acceptAlwaysShown
orShownIfRoom
items here. - Our method to split the items receives a list of
ActionMenuItem
s, the interface representing our action menu items, and the number of items we want to show in theTopAppBar
. - Next we split the items into 3 buckets, leveraging their types, one of the advantages of using
sealed class
es to represent the action items. We convert the lists forAlwaysShown
andShownIfRoom
items to mutable lists because we may need to promote someShownIfRoom
items toAlwaysShown
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 ofAlwaysShown
items plus the number ofShownIfRoom
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 allShownIfRoom
items to beAlwaysShown
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 theAlwaysShown
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 theAlwaysShown
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 theAlwaysShown
list, and remove them from theShownIfRoom
list. In other words, we promote as many items from theShownIfRoom
list to theAlwaysShown
list as available slots we have. - Finally we have our result, the items that will display in the
TopAppBar
are those that were of typeAlwaysShown
plus the items of typeShownIfRoom
that were promoted, and the overflow items are the reminder of theShownIfRoom
items, plus all of theNeverShown
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 ofActionMenuItem
, 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 theTopAppBar
. 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 theitems
and themaxVisibleItems
do not change, so we use those 2 attributes as keys on theremember
method. - After this we iterate over the
AlwaysShown
items and add them to theTopAppBar
, 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 theisOpen
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 theactions
slot on theTopAppBar
. - We provide a list of action items, some of type
AlwaysShown
, others of typeShownIfRoom
, and finally others of typeNeverShown
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.
Thanks for reading, I hope you found this useful and can use it in your apps. Below is the full implementation with some previews, for different scenarios mixing the 3 types of action menu items. Happy coding!