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