Android Navigation multiple start destinations

The Android Navigation Architecture Components currently only support a single start destination which doesn’t always make sense when using drawer style navigation which can have multiple destinations that are on the same level.

The NavigationUI.setupActionBarWithNavController method syncs the navigation with the drawer hamburger menu in the action bar but it shows the back arrow instead of the hamburger icon for all destinations that aren’t the start one which is confusing. A better behaviour would be to only show the back arrow for destinations that aren’t top level drawer and / or bottom menu destinations.

After a bit of investigating it’s possible to change the behaviour by changing the NavigationUI.setupActionBarWithNavController and NavigationUI.navigateUp methods to add support for multiple start / top level destinations. There are other parts of the code that use the start destination but these two are the ones which affect the behaviour of the menu icon when using a drawer.

So as a workaround I’ve created a small class which takes a list of start / top level destination IDs and then updates the NavigationUI.setupActionBarWithNavController and NavigationUI.navigateUp methods to use them.

The class also has an onBackPressed() method which will close the activity if the current destination is one of the start / top level destinations which might make sense or might not depending on your app. If no used, pressing the back button will navigate to the single start destination when on one of the top level destinations.

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.view.Gravity
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.core.app.ActivityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.NavDestination

class MultiStartNavigationUI(private val startDestinations: List<Int>) {
    fun setupActionBarWithNavController(activity: AppCompatActivity, navController: NavController,
                                        drawerLayout: DrawerLayout?) {

        navController.addOnNavigatedListener(ActionBarOnNavigatedListener(
                activity, startDestinations, drawerLayout))
    }

    fun navigateUp(drawerLayout: DrawerLayout?, navController: NavController): Boolean {
        if (drawerLayout != null && startDestinations.contains(navController.currentDestination.id)) {
            drawerLayout.openDrawer(Gravity.START)
            return true
        } else {
            return navController.navigateUp()
        }
    }

    fun onBackPressed(activity: AppCompatActivity,
                      navController: NavController): Boolean {
        if (startDestinations.contains(navController.currentDestination.id)) {
            ActivityCompat.finishAfterTransition(activity)
            return true
        }

        return false
    }

    private class ActionBarOnNavigatedListener(
            private val mActivity: AppCompatActivity,
            private val startDestinations: List<Int>,
            private val mDrawerLayout: DrawerLayout?
    ) : NavController.OnNavigatedListener {
        private var mArrowDrawable: DrawerArrowDrawable? = null
        private var mAnimator: ValueAnimator? = null

        override fun onNavigated(controller: NavController, destination: NavDestination) {
            val actionBar = mActivity.supportActionBar

            val title = destination.label
            if (!title.isNullOrEmpty()) {
                actionBar?.title = title
            }

            val isStartDestination = startDestinations.contains(destination.id)
            actionBar?.setDisplayHomeAsUpEnabled(this.mDrawerLayout != null || !isStartDestination)
            setActionBarUpIndicator(mDrawerLayout != null && isStartDestination)
        }


        private fun setActionBarUpIndicator(showAsDrawerIndicator: Boolean) {
            val delegate = mActivity.drawerToggleDelegate
            var animate = true
            if (mArrowDrawable == null) {
                mArrowDrawable = DrawerArrowDrawable(delegate!!.actionBarThemedContext)
                delegate.setActionBarUpIndicator(mArrowDrawable, 0)
                animate = false
            }

            mArrowDrawable?.let {
                val endValue = if (showAsDrawerIndicator) 0.0f else 1.0f

                if (animate) {
                    val startValue = it.progress
                    mAnimator?.cancel()

                    @SuppressLint("ObjectAnimatorBinding")
                    mAnimator = ObjectAnimator.ofFloat(it, "progress", startValue, endValue)
                    mAnimator?.start()
                } else {
                    it.progress = endValue
                }
            }

        }
    }
}

Example usage:

class MainActivity : AppCompatActivity() {
    private lateinit var navController: NavController
    private val multiStartNavigationUi = MultiStartNavigationUI(listOf(
            R.id.startIdA,
            R.id.startIdB
    ))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setSupportActionBar(findViewById(R.id.toolbar))

        navController = findNavController(this, R.id.nav_host_fragment)

        // Replace NavigationUI.setupActionBarWithNavController with the
        // multiple start destination one
        multiStartNavigationUi.setupActionBarWithNavController(this, navController, drawer)

        NavigationUI.setupWithNavController(findViewById<NavigationView>(R.id.navigation),
                navController)
    }

    override fun onBackPressed() {
        // Optional if you want the app to close when the back button is pressed
        // on a start destination
        if (!multiStartNavigationUi.onBackPressed(this, navController)) {
            super.onBackPressed()
        }
    }

    // Replace NavigationUI.navigateUp with the multiple start destination one
    override fun onSupportNavigateUp() = multiStartNavigationUi.navigateUp(drawer, navController)
}

Hopefully, Google will add official support for multiple start / top level destinations but for now this workaround seems to work well.

Comments