本文介绍了如何在BottomNavigationView中使用动态功能模块?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用类似下面的导航图添加动态功能,并且可以与viewPager2或其他片段配合使用,但不能与 BottomNavigationView 一起使用.

Adding dynamic feature using a navigation graph like the one below and works fine with viewPager2, or another fragment, but not with a BottomNavigationView.

布局

<?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>

导航到动态特征模块的 BottomNavigationView 标签的导航图

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>

由于 BottomNavigationView 的选项卡没有单独的片段后退堆栈,因此此类用于后退导航.

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"

当您将动态功能添加到任何nav_graph时,此 BottomNavigationView setUpWithNavContoller 方法使用下面的代码段

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
    )

您遇到错误

 Caused by: java.lang.IllegalStateException: Could not find Navigator with name "include-dynamic". You must call NavController.addNavigator() for each navigation type.

如果您使用 obtainNavHostFragment 方法中的 DynamicNavHostFragment 更改 NavHostFragment

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
}

你得到

java.lang.ClassCastException: androidx.navigation.fragment.NavHostFragment cannot be cast to androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment

因为 NavHostFragment.create()返回 NavHostFragment ,而不是类型 extends NavHostFragment

since NavHostFragment.create() returns NavHostFragment instead of type extends NavHostFragment

public static NavHostFragment create(@NavigationRes int graphResId) {
    return create(graphResId, null);
}

我问了它这里

是否可以仅使用 BottomNavigationView 并为每个选项卡添加后退堆栈来实现动态功能?

Is it possible to implement dynamic features with only BottomNavigationView with back stack for each tab?

推荐答案

此处是一个工作示例,如果您想查看的话,此答案中的实现为 MainFragmentBottomNav . MainFragment 使用 ViewPager2 设置BottomNavigation选项卡.

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.

创建一个扩展 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
            }
        }
    }
}

下面的常量映射到 NavHostFragment 中的值,我在 NavHostFragment 中使用了相同的值,因此当检查参数时,它将像

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()函数与NavHostFragment.create()相同,但返回 DynamicNavHostFragment 而不是 NavHostFragment().您也可以取消注释行和setGraph的注释并检查其他条件,但是对const值使用替代方法可以使其更简单. KEY_GRAPH_ID KEY_START_DESTINATION_ARGS 的值与NavHostFragment源代码匹配.

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.

在那之后,更改所提供的getNavHost函数NavigationExtension.kt中的一行.

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
}

更改为 BaseDynamicNavHostFragment.createDynamicNavHostFragment(navGraphId)

最后在应用程序导航文件夹中创建空的导航图,这些导航图使用< include-dynamic>

Finally in app navigation folder create empty nav graphs which define start destinations as dynamic nav host fragment nav graphs with <include-dynamic>

例如应用模块中的 nav_graph_dash_board_start.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"
    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>

以及动态功能模块 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>

这篇关于如何在BottomNavigationView中使用动态功能模块?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

06-07 20:21