多模块项目的导航最佳实践

导航图可以包含以下任何组合

  • 单个目标,例如 <fragment> 目标。
  • 一个 嵌套图,它封装了一组相关目标。
  • 一个 <include> 元素,它允许您嵌入另一个导航图文件,就像它嵌套一样。

这种灵活性使您能够将较小的导航图组合在一起以形成应用的完整导航图,即使这些较小的导航图由单独的 模块 提供。

在本主题的示例中,每个 功能模块 都专注于一个 功能,并提供一个封装实现该功能所需的所有目标的单个导航图。在生产应用中,您可能在较低级别有许多子模块,这些子模块是此高级功能模块的实现细节。这些功能模块中的每一个都直接或间接地包含在您的 app 模块 中。本文档中使用的示例 多模块应用 具有以下结构

dependency graph for a sample multi-module application
the start destination of the example app
图 1. 示例应用的应用架构和启动目标。

每个功能模块都是一个自包含的单元,具有自己的导航图和目标。app 模块依赖于每个模块,将其作为实现细节添加到其 build.gradle 文件中,如下所示

Groovy

dependencies {
    ...
    implementation project(":feature:home")
    implementation project(":feature:favorites")
    implementation project(":feature:settings")

Kotlin

dependencies {
    ...
    implementation(project(":feature:home"))
    implementation(project(":feature:favorites"))
    implementation(project(":feature:settings"))

app 模块的作用

app 模块负责提供应用的完整图,并将 NavHost 添加到您的 UI 中。在 app 模块的导航图中,您可以使用 <include> 来引用库图。虽然使用 <include> 在功能上与使用嵌套图相同,但 <include> 支持来自其他项目模块或库项目的图,如下例所示

<?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"
    app:startDestination="@id/home_nav_graph">

    <include app:graph="@navigation/home_navigation" />
    <include app:graph="@navigation/favorites_navigation" />
    <include app:graph="@navigation/settings_navigation" />
</navigation>

将库包含到顶级导航图后,您可以根据需要 导航 到库图。例如,您可以创建一个操作,从导航图中的片段导航到设置图,如下所示

<?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"
    app:startDestination="@id/home_nav_graph">

    <include app:graph="@navigation/home_navigation" />
    <include app:graph="@navigation/favorites_navigation" />
    <include app:graph="@navigation/settings_navigation" />

    <fragment
        android:id="@+id/random_fragment"
        android:name="com.example.android.RandomFragment"
        android:label="@string/fragment_random" >
        <!-- Launch into Settings Navigation Graph -->
        <action
            android:id="@+id/action_random_fragment_to_settings_nav_graph"
            app:destination="@id/settings_nav_graph" />
    </fragment>
</navigation>

当多个特性模块需要引用一组通用的目标(例如登录图)时,您**不应**将这些通用目标包含到每个特性模块的导航图中。相反,将这些通用目标添加到您的 app 模块的导航图中。然后,每个特性模块都可以 跨特性模块导航 以导航到这些通用目标。

在前面的示例中,操作指定了 @id/settings_nav_graph 的导航目标。此 ID 指的是在包含的图 @navigation/settings_navigation 中定义的目标。

应用模块中的顶级导航

导航组件包含一个 NavigationUI 类。此类包含管理与应用顶部应用栏、导航抽屉和底部导航的导航的静态方法。如果应用的顶级目标由特性模块提供的 UI 元素组成,则 app 模块是放置顶级导航和 UI 元素的自然位置。由于应用模块依赖于协作的特性模块,因此所有目标都可从应用模块中定义的代码访问。这意味着,如果项目的 ID 与目标的 ID 匹配,则可以使用 NavigationUI目标绑定到菜单项

在图 2 中,示例 app 模块在其主活动中定义了一个 BottomNavigationView。菜单中的菜单项 ID 与库图的导航图 ID 匹配

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@id/home_nav_graph"
        android:icon="@drawable/ic_home"
        android:title="Home"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@id/favorites_nav_graph"
        android:icon="@drawable/ic_favorite"
        android:title="Favorites"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@id/settings_nav_graph"
        android:icon="@drawable/ic_settings"
        android:title="Settings"
        app:showAsAction="ifRoom" />
</menu>

要让 NavigationUI 处理 底部导航,请从主活动类中的 onCreate() 调用 setupWithNavController(),如下例所示

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController

    findViewById<BottomNavigationView>(R.id.bottom_nav)
            .setupWithNavController(navController)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    NavHostFragment navHostFragment =
            (NavHostFragment) supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);

    NavigationUI.setupWithNavController(bottomNav, navController);
}

有了这段代码,当用户点击底部导航项时,NavigationUI 会导航到相应的库图。

请记住,让应用模块硬依赖于特性模块导航图中深度嵌套的特定目标通常是不好的做法。在大多数情况下,您希望应用模块只了解任何嵌入式或包含的导航图的入口点(这同样适用于特性模块之外的情况)。如果您需要链接到库导航图中深层嵌套的目标,首选的方法是使用 深层链接。深层链接也是库导航到另一个库导航图中目标的唯一方法。

跨特性模块导航

在编译时,独立的特性模块无法相互查看,因此您无法使用 ID 导航到其他模块中的目标。相反,使用深层链接 直接导航到与 隐式深层链接 关联的目标。

继续前面的示例,假设您需要从 :feature:home 模块中的按钮导航到嵌套在 :feature:settings 模块中的目标。您可以通过向设置导航图中的目标添加深层链接来实现,如下所示

<?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/settings_nav_graph"
    app:startDestination="@id/settings_fragment_one">

    ...

    <fragment
        android:id="@+id/settings_fragment_two"
        android:name="com.example.google.login.SettingsFragmentTwo"
        android:label="@string/settings_fragment_two" >

        <deepLink
            app:uri="android-app://example.google.app/settings_fragment_two" />
    </fragment>
</navigation>

然后,将以下代码添加到主页片段中按钮的 onClickListener

Kotlin

button.setOnClickListener {
    val request = NavDeepLinkRequest.Builder
        .fromUri("android-app://example.google.app/settings_fragment_two".toUri())
        .build()
    findNavController().navigate(request)
}

Java

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        NavDeepLinkRequest request = NavDeepLinkRequest.Builder
            .fromUri(Uri.parse("android-app://example.google.app/settings_fragment_two"))
            .build();
        NavHostFragment.findNavController(this).navigate(request);
    }
});

与使用操作或目标 ID 导航不同,您可以导航到任何图中的任何 URI,即使跨模块也是如此。

使用 URI 导航时,不会重置返回堆栈。此行为与 显式深层链接导航 不同,在显式深层链接导航中,导航时会替换返回堆栈。