You can create two navigation graphs to achieve the behavior you want. One for the top level destinations and a second one for the modal sheet. They need to be independent and do not have any links between each other. You can't use only one nav graph as the "navigation surface" is a different one. For the main navigation it's the activity and for the modal bottom sheet it's the bottom sheets window (which is in case of a BottomSheetDialogFragment actually a different window).
In theory this can be achieved very easily:
main_nav.xml
holds Settings
, NoteList
and Trash
filter_nav.xml
holds the FilterMenu
, Search
, and TagList
If you don't want back navigation on the top level you can even do the top level without a navigation controller using fragment transactions.
So basically you need a (BottomSheet)DialogFragment
which needs an seperate NavHost
independent from the main/other NavHost
. You can achieve this with following class:
dialog_fragment_modal_bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/filterNavHost"/>
ModalBottomSheetDialogFragment .kt
class ModalBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.dialog_fragment_modal_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// We can't inflate the NavHostFragment from XML because it will crash the 2nd time the dialog is opened
val navHost = NavHostFragment()
childFragmentManager.beginTransaction().replace(R.id.filterNavHost, navHost).commitNow()
navHost.navController.setGraph(R.navigation.filter_nav)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
// Normally the dialog would close on back press. We override this behaviour and check if we can navigate back
// If we can't navigate back we return false triggering the default implementation closing the dialog
setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
view?.findNavController()?.popBackStack() == true
} else {
false
}
}
}
}
}
We do two tricks here:
We need to manually create the NavHost
fragment. If we directly put it into XML, it will crash the second time the dialog is opened as the ID is already used
We need to overwrite the dialog's back navigation. A dialog is a separate window on top of your activity, so the Activity
's onBackPressed()
gets not called. Instead, we add a OnKeyListener
and when the back button is released (ACTION_UP
) we check with the NavController
whether it can pop the back stack (go back) or not. If it can pop the back stack we return true and thus consume the back event. The dialog stays open and the NavController
goes one step back. If it is already at the starting point, the dialog will close as we return false.
You can now create a nested graph inside the dialog and not care about the outer graph. To show the dialog with the nested graph use:
val dialog = ModalBottomSheetDialogFragment()
dialog.show(childFragmentManager, "filter-menu")
You could also add the ModalBottomSheetDialogFragment
as <dialog>
destination in main_nav
, I did not test this though. This feature is currently still in alpha and was introduced in navigation 2.1.0-alpha03. Because this is still in alpha, the API might change and I'd personally use the code above to show the dialog. As soon as this is out of alpha/beta, using a destination in main_nav.xml
should be the preferred way. The different way to show the dialog makes no difference from a user's perspective.
I create a sample application with your navigation structure here on GitHub. It has working back navigation on both levels with the two independent graphs. You can see it working here on Youtube. I used a bottom bar for the main navigation, but you can replace it with a drawer instead.