android - 如何将动态功能模块与 BottomNavigationView 一起使用?

标签 android bottomnavigationview android-architecture-navigation dynamic-feature-module dynamic-feature

使用如下所示的导航图添加动态功能,并且可以与 viewPager2 或其他 fragment 一起正常工作,但不能与 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 选项卡的导航图应该导航到动态功能模块
<?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它的选项卡没有单独的 fragment 后退堆栈,并且此类用于后退导航。
/**
 * 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使用以下代码段的方法
   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.
如果您更改 NavHostFragmentDynamicNavHostFragmentobtainNavHostFragment方法
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
public static NavHostFragment create(@NavigationRes int graphResId) {
    return create(graphResId, null);
}
我问了here
是否可以仅使用 BottomNavigationView 实现动态功能?每个选项卡都有回栈?

最佳答案

Here如果您想 checkout ,这是一个工作示例,此答案中的实现是 MainFragmentBottomNav . MainFragment使用 ViewPager2设置底部导航选项卡。
这有点像一种解决方法,但它确实有效。
创建一个扩展 DynamicNavHostFragment 的 fragment .

/**
 * [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 中使用了相同的值所以当检查参数时,它会像
`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 源代码匹配。
之后,在相关提供的 gainNavHost 函数 NavigationExtension.kt 中更改一行。
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)最后在应用程序导航文件夹中创建空导航图,将起始目的地定义为动态导航主机 fragment 导航图 <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>

关于android - 如何将动态功能模块与 BottomNavigationView 一起使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62796133/

相关文章:

android - 如何使用底部导航 View 和 Android 导航组件将参数传递给 fragment ?

android - 将 PlaceAutocompleteFragment 添加到 fragment 会引发错误

android - Android (2D) Canvas 绘图管道的各个部分如何组合在一起?

Android:同时录制原始音频和录制视频

android - 如何强制 BottomNavigationView 标签为多行

java - 在 BottomNavigationView 项目触摸时,Fragment 中的 RecyclerView 有时为空

android - IllegalArgumentException : navigation destination xxx is unknown to this NavController

Android Espresso 如何测试从不同路径流向特定 Activity

android - BottomNavigationView 不存在

android - Cause : androidx. navigation.safeargs 只能与androidx项目一起使用