Android Jetpack Navigation: Add Navigation Drawer (DrawerLayout)

October 16, 2019

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:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.