Android Jetpack Navigation: Add Navigation Drawer (DrawerLayout)

Layout

This is the main content layout. There could be a upper hierarchy layout which hold CoordinatorLayout, AppBarLayout and Toolbar.

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/mobile_navigation"

        />

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:menu="@menu/test_drawer"
        />
</androidx.drawerlayout.widget.DrawerLayout>

res/menu/test_drawer

<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/action_item1"
        android:title="Item 1" />
    <item
        android:id="@+id/action_item2"
        android:title="Item 2" />
</menu>

Activity

class MainActivity : AppCompatActivity() {    private val navController by lazy { findNavController(R.id.nav_host_fragment) }    private lateinit var appBarConfiguration: AppBarConfiguration    override fun onCreate(savedInstanceState: Bundle?) {        ...        val appBarConfiguration = AppBarConfiguration(            setOf(                R.id.home, R.id.search, R.id.profile // top level destinations            ),            drawerLayout // Navigation Drawer        )        setupActionBarWithNavController(navController, appBarConfiguration)        navView.setupWithNavController(navController)  // Navigation Drawer        bottomNavView.setupWithNavController(navController)    }    override fun onSupportNavigateUp(): Boolean {        // return navController.navigateUp()        // This will misbehave when using with BottomNavigationView: after clicking on 2nd or 3rd Tab, the Hamburger icon trigger back        // instead of showing Drawer        // return navController.navigateUp(drawerLayout) || super.onSupportNavigateUp()        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()    }    // enable close drawer on back pressed    override fun onBackPressed() {        if (this.drawerLayout.isDrawerOpen(GravityCompat.START)) {            this.drawerLayout.closeDrawer(GravityCompat.START)        } else {            super.onBackPressed()        }    }}

Show/Hide Drawer on specific fragment

I wanted to achive the following

  • Hide drawer on main bottomNavView fragments (R.id.home, R.id.search, R.id.profile)
  • Show drawer on specific fragment (R.id.import_photos)

Add R.id.import_photos as top level destinations for drawer to be shown.

val appBarConfiguration = AppBarConfiguration(    setOf(        R.id.home, R.id.search, R.id.profile, R.id.import_photos // top level destinations    ),    drawerLayout)

Seems like the only way to hide the Drawer is via supportActionBar?.setDisplayHomeAsUpEnabled(false) (actually hiding the button which trigger the drawer).

navController.addOnDestinationChangedListener { controller, destination, arguments ->    when(destination.id) {        R.id.home, R.id.search, R.id.profile -> { // bottomNavView            supportActionBar?.setDisplayHomeAsUpEnabled(false)            bottomNavView.isVisible = true        }        R.id.import_photos -> {            supportActionBar?.setDisplayHomeAsUpEnabled(true)            // dynamic loading of menu            navView.menu.clear()            navView.inflateMenu(R.menu.import_photos_drawer)            bottomNavView.isGone = true        }        else -> {            bottomNavView.isGone = true        }    }}

/res/menu/import_photos_drawer.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/action_gallery"
        android:title="Gallery"
        android:checkable="true" />
    <item
        android:id="@+id/action_google_photos"
        android:title="Google Photos"
        android:checkable="true" />
</menu>

Handle Drawer/NavigationView Item Click in Activity

navView.setNavigationItemSelectedListener { item ->    drawerLayout.closeDrawer(GravityCompat.START, false)    // navView.setCheckedItem(item)    when(item.itemId) {        R.id.action_gallery -> {            supportActionBar?.title = "Gallery"            true        }        R.id.action_google_photos -> {            supportActionBar?.title = "Google Photos"            true        }        else -> false    }}

Handle Drawer/NavigationView Item Click in Fragment

Direct access activity (tight coupling)

class ImportPhotosFragment : Fragment() {    override fun onActivityCreated(savedInstanceState: Bundle?) {        super.onActivityCreated(savedInstanceState)        // ...        val activity = activity as? AppCompatActivity        activity?.navView?.setNavigationItemSelectedListener { item ->            activity.drawerLayout.closeDrawer(GravityCompat.START, false)            when(item.itemId) {                R.id.action_gallery -> {                    activity.supportActionBar?.title = "Gallery"                    true                }                R.id.action_google_photos -> {                    activity.supportActionBar?.title = "Google Photos"                    true                }                else -> false            }        }    }}

Via LiveData

class MainActivity : AppCompatActivity() {    private val viewModel by viewModels<MainViewModel>()    override fun onCreate(savedInstanceState: Bundle?) {        // ...        navView.setNavigationItemSelectedListener { item ->            drawerLayout.closeDrawer(GravityCompat.START, false)            viewModel.navigationDrawerItemClickEvent.value = Event(item.itemId)            true        }        viewModel.titleEvent.observe(this, Observer { event ->            event.getIfPending()?.also { title ->                supportActionBar?.title = title            }        })    }}
class MainViewModel : ViewModel() {    internal val navigationDrawerItemClickEvent = MutableLiveData<Event<Int>>()    internal val titleEvent = MutableLiveData<Event<String>>()}
class ImportPhotosFragment : Fragment() {    private val viewModel by viewModels<ImportPhotosViewModel>()    private val mainViewModel by activityViewModels<MainViewModel>()    override fun onActivityCreated(savedInstanceState: Bundle?) {        super.onActivityCreated(savedInstanceState)        mainViewModel.navigationDrawerItemClickEvent.observe(this, Observer { event ->            event.getIfPending()?.also { menuId ->                when(menuId) {                    R.id.action_gallery -> {                        mainViewModel.titleEvent.value = Event("Gallery")                    }                    R.id.action_google_photos -> {                        mainViewModel.titleEvent.value = Event("Google Photos")                    }                }            }        })    }}

NOTE: Refer Get ViewModel via KTX.

References:

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.