Please look at the flowchart I made if you have difficulty in understanding the following paragraph.
I'm currently making a notes app with 3 top level destinations. One of the top-level destinations(NotesList) displays a list of notes created by the user. NotesList has a filter button which brings up a bottom modal sheet with FilterMenu destination. FilterMenu has a search button, which on clicking, replaces the contents of the sheet with a Search destination and a button named tags which on clicking, replaces the contents of the sheet with a fragment containing list of the tags associated with all the notes(TagList destination).
Everything in blue is a top level destination. Everything in purple is present in the modal sheet.
The FilterMenu, Search and the TagList are displayed in a modal sheet. Which means that the NotesList contains these fragments and is not replaced by them. They exist in a region of screen smaller than the NotesList. If I use navigation, the fragments will replace each other.
Can I use two NavHosts? One for the top-level destinations and one for the stuff in the modal sheet? If so, how would I implement it? If not, what's the recommended thing to do in this case?
To take full advantage of the Navigation component, your app should use multiple fragments in a single activity. However, activities can still benefit from the Navigation component. Note, however, that your app's UI must be visually broken up across several navigation graphs.
The Navigation component contains a default NavHost implementation, NavHostFragment , that displays fragment destinations. NavController : An object that manages app navigation within a NavHost . The NavController orchestrates the swapping of destination content in the NavHost as users move throughout your app.
Nav Host: The Nav Host is an empty container that displays all destinations from your navigation graph. The navigation component has a default navhost implementation which displays the fragment destinations. NavController: The NavController is an object that manages app navigation within a Nav Host.
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.
An easy way to create two navHostFragments is to create another navigation.xml file.
In my app for example I have two navHostsFragments.
I defined the first one for navigation flow so when the user enters the app he goes to the login fragment which is the navHostFragment.
After user logs in he is transferred to the mainActivity which contains my new navHostFragment.
That way whenever I start a new activity that contains my new navHostFragment.
Hope this helps
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With