A navigation drawer with submenu in Android remains one of the most effective design patterns for organizing complex application hierarchies. While the standard navigation drawer handles flat lists of destinations effortlessly, modern apps often require nested categories—think settings grouped by function, product catalogs divided by department, or multi-module enterprise tools. In real terms, implementing expandable groups inside the drawer requires a solid understanding of the NavigationView, custom adapters, or the modern NavigationRail and ModalNavigationDrawer composables in Jetpack Compose. This guide walks through the architecture, XML implementation, and Compose equivalents needed to build a reliable, accessible, and maintainable expandable navigation drawer.
Understanding the Core Components
Before diving into code, it is crucial to identify the moving parts. Consider this: a standard drawer relies on a DrawerLayout as the root container, a NavigationView for the menu content, and a menu resource XML defining items. To support submenus, the data model must shift from a flat list to a tree structure: Parent Items (Headers/Groups) containing Child Items (Destinations) That's the whole idea..
In the traditional View system, the NavigationView does not natively support expandable lists with smooth animations out of the box. Also, developers typically choose between two approaches: using a RecyclerView inside the NavigationView header/layout with a custom ExpandableRecyclerAdapter, or leveraging a third-party library like MaterialDrawer or AdvancedNavigationDrawer. For a dependency-free, lightweight solution, a custom RecyclerView implementation offers the most control over UI states, click handling, and animation logic Simple as that..
Building the Data Model
A clean data model separates the UI logic from the data structure. Create a sealed class or interface hierarchy to represent the two distinct view types: the Group (expandable header) and the Item (navigation destination).
sealed interface DrawerItem {
data class Group(
val title: String,
@DrawableRes val iconRes: Int,
val children: List,
var isExpanded: Boolean = false
) : DrawerItem
data class Child(
val title: String,
@DrawableRes val iconRes: Int,
val destinationRoute: String // Or Menu Item ID
) : DrawerItem
}
This structure allows the adapter to differentiate view types via instanceof checks (or when expressions in Kotlin) and manage the expansion state directly inside the Group object. Storing isExpanded in the model simplifies state restoration during configuration changes, provided you use ViewModel or onSaveInstanceState That's the part that actually makes a difference. But it adds up..
Short version: it depends. Long version — keep reading.
Implementing the Custom Adapter
The heart of the submenu logic lives in a RecyclerView.Adapter capable of handling two view holders: GroupViewHolder and ChildViewHolder.
The View Holders
The GroupViewHolder binds the parent title, icon, and a rotation-capable expand/collapse indicator (typically a chevron icon). The ChildViewHolder binds the destination label and icon, usually indented visually to denote hierarchy.
class GroupViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.tv_group_title)
val icon: ImageView = view.findViewById(R.id.iv_group_icon)
val arrow: ImageView = view.findViewById(R.id.iv_arrow_indicator)
val container: View = view.findViewById(R.id.group_container)
}
class ChildViewHolder(view: View) : RecyclerView.id.In real terms, findViewById(R. Which means findViewById(R. Even so, id. Here's the thing — findViewById(R. And iv_child_icon)
val container: View = view. But tv_child_title)
val icon: ImageView = view. And viewHolder(view) {
val title: TextView = view. id.
### The Adapter Logic
Override `getItemViewType` to return distinct integers for Groups and Children. In `onBindViewHolder`, the critical logic resides in the Group binding:
1. **Rotate Arrow:** Animate the arrow icon rotation (0 to 180 degrees) based on `group.isExpanded`.
2. **Toggle Visibility:** When the group container is clicked, toggle `group.isExpanded`.
3. **Notify Range Changes:** Calculate the index range of children immediately following the group in the flattened list. Call `notifyItemRangeInserted` or `notifyItemRangeRemoved` for smooth default animations.
```kotlin
inner class DrawerAdapter(private val items: MutableList) :
RecyclerView.Adapter() {
// ... ViewType constants and onCreateViewHolder ...
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
when (holder) {
is GroupViewHolder -> bindGroup(holder, item as DrawerItem.Group, position)
is ChildViewHolder -> bindChild(holder, item as DrawerItem.
private fun bindGroup(holder: GroupViewHolder, group: DrawerItem.And group, position: Int) {
holder. title.text = group.But title
holder. icon.Here's the thing — setImageResource(group. iconRes)
holder.arrow.In practice, rotation = if (group. isExpanded) 180f else 0f
holder.container.setOnClickListener {
group.That said, isExpanded = ! group.Worth adding: isExpanded
val startIndex = position + 1
val childCount = group. children.Worth adding: size
if (group. isExpanded) {
items.addAll(startIndex, group.That said, children)
notifyItemRangeInserted(startIndex, childCount)
} else {
val endIndex = startIndex + childCount
val removedItems = items. On the flip side, subList(startIndex, endIndex)
items. Now, removeAll(removedItems)
notifyItemRangeRemoved(startIndex, childCount)
}
}
}
private fun bindChild(holder: ChildViewHolder, child: DrawerItem. Now, child) {
holder. title.text = child.Here's the thing — title
holder. icon.setImageResource(child.Consider this: iconRes)
holder. container.setOnClickListener {
// Handle Navigation: Close drawer, work through to child.destinationRoute
onItemClick(child.
**Pro Tip:** To prevent visual glitches during rapid clicks, disable the click listener briefly during animation or use `DiffUtil` with `ListAdapter` for a more dependable, modern approach.
## Integrating with DrawerLayout and NavigationView
Place the `RecyclerView` inside the `NavigationView` layout file (`nav_view.xml`). Remove the standard `app:menu` attribute from the `NavigationView` XML, as the RecyclerView replaces the native menu inflation.
```xml