问题
Adding dynamic feature using a navigation graph like the one below and works fine with viewPager2, or another fragment, but not with a BottomNavigationView
.
Layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/nav_host_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_nav"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu_bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Nav graph for a tab of BottomNavigationView
that should navigate to dynamic feature module
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_dashboard"
app:startDestination="@id/dashboardFragment1">
...
<!-- photos dynamic feature module-->
<include-dynamic
android:id="@+id/nav_graph_photos"
android:name="com.abc.photos"
app:graphResName="nav_graph_photos"
app:moduleName="photos">
<argument
android:name="count"
android:defaultValue="0"
app:argType="integer" />
</include-dynamic>
</navigation>
Since BottomNavigationView
has no individual back stack for fragments for it's tabs and this class is used for back navigation.
/**
* Manages the various graphs needed for a [BottomNavigationView].
*
* This sample is a workaround until the Navigation Component supports multiple back stacks.
*/
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent)
&& selectedItemId != navHostFragment.navController.graph.id
) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
When you add dynamic feature to any nav_graph this BottomNavigationView
setUpWithNavContoller
method using snippet below
val navGraphIds = listOf(
R.navigation.nav_graph_home,
R.navigation.nav_graph_dashboard,
R.navigation.nav_graph_notification
)
// Setup the bottom navigation view with a list of navigation graphs
val controller = bottomNavigationView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = childFragmentManager,
containerId = R.id.nav_host_container,
intent = requireActivity().intent
)
You get error
Caused by: java.lang.IllegalStateException: Could not find Navigator with name "include-dynamic". You must call NavController.addNavigator() for each navigation type.
If you change NavHostFragment
with DynamicNavHostFragment
in obtainNavHostFragment
method
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): DynamicNavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as DynamicNavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId) as DynamicNavHostFragment
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
you get
java.lang.ClassCastException: androidx.navigation.fragment.NavHostFragment cannot be cast to androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment
since NavHostFragment.create()
returns NavHostFragment
instead of type extends
NavHostFragment
public static NavHostFragment create(@NavigationRes int graphResId) {
return create(graphResId, null);
}
I asked about it here
Is it possible to implement dynamic features with only BottomNavigationView
with back stack for each tab?
回答1:
Here is a working sample if you wish to check out, implementation in this answer is MainFragmentBottomNav
. MainFragment
uses ViewPager2
to set BottomNavigation tabs.
This is a little bit like a workaround but it works.
Create a fragment that extends DynamicNavHostFragment
.
/**
* [DynamicNavHostFragment] creator class which
* uses [BaseDynamicNavHostFragment.createDynamicNavHostFragment] function with navigation graph
* parameter
*/
class BaseDynamicNavHostFragment : DynamicNavHostFragment() {
private val navControllerViewModel by activityViewModels<NavControllerViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// val navResId = arguments?.get(KEY_GRAPH_ID) as Int
// val startDestinationArgs = arguments?.get(KEY_START_DESTINATION_ARGS) as? Bundle
//
// findNavController().setGraph(navResId, startDestinationArgs)
// }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onResume() {
super.onResume()
// Set this navController as ViewModel's navController
navControllerViewModel.currentNavController.value = Event(navController)
}
override fun onDestroyView() {
navControllerViewModel.currentNavController.value = Event(null)
super.onDestroyView()
}
companion object {
private const val KEY_GRAPH_ID = "android-support-nav:fragment:graphId"
private const val KEY_START_DESTINATION_ARGS =
"android-support-nav:fragment:startDestinationArgs"
/**
* Create a new NavHostFragment instance with an inflated [NavGraph] resource.
*
* @param graphResId resource id of the navigation graph to inflate
* @param startDestinationArgs arguments to send to the start destination of the graph
* @return a new NavHostFragment instance
*/
@JvmStatic
fun createDynamicNavHostFragment(
@NavigationRes graphResId: Int,
startDestinationArgs: Bundle? = null
): BaseDynamicNavHostFragment {
if (graphResId == 0) throw NavigationException("Navigation graph id cannot be 0")
val bundle: Bundle = Bundle().apply {
putInt(KEY_GRAPH_ID, graphResId)
if (startDestinationArgs != null) {
putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs)
}
}
return BaseDynamicNavHostFragment().apply {
arguments = bundle
}
}
}
}
The constants below are mapped to values from NavHostFragment
, i used the same values in NavHostFragment
so when arguments are checked it would work like
`NavHostFragment.create()` which has a source code
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs) {
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}
final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
BaseDynamicNavHostFragmen().createDynamicNavHostFragment()
function is identical as as NavHostFragment.create() but returns DynamicNavHostFragment
instead of NavHostFragment()
. You can also uncomment the lines and setGraph and check other conditions but using workaround for const values makes it simpler. The values of KEY_GRAPH_ID
, and KEY_START_DESTINATION_ARGS
match with NavHostFragment source code.
After that change one line in obtainNavHost function NavigationExtension.kt which is provided in question.
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = BaseDynamicNavHostFragment.createDynamicNavHostFragment(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
the change is BaseDynamicNavHostFragment.createDynamicNavHostFragment(navGraphId)
Finally in app navigation folder create empty nav graphs which define start destinations as dynamic nav host fragment nav graphs with <include-dynamic>
for instance nav_graph_dash_board_start.xml in app module
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph_dashboard_start"
app:startDestination="@id/nav_graph_dashboard">
<!-- Dashboard dynamic feature module -->
<include-dynamic
android:id="@+id/nav_graph_dashboard"
android:name="com.smarttoolfactory.dashboard"
app:graphResName="nav_graph_dashboard"
app:moduleName="dashboard">
</include-dynamic>
</navigation>
And in dynamic feature module nav_graph_dashboard.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@id/nav_graph_dashboard"
app:moduleName="dashboard"
app:startDestination="@id/dashboardFragment">
<fragment
android:id="@+id/dashboardFragment"
android:name="com.smarttoolfactory.dashboard.DashboardFragment"
android:label="Dashboard Fragment"
tools:layout="@layout/fragment_dashboard" />
</navigation>
来源:https://stackoverflow.com/questions/62796133/how-to-use-dynamic-feature-module-with-bottomnavigationview