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

导航图可以由以下项的任意组合构成:

  • 单个目的地,例如 <fragment> 目的地。
  • 封装了一组相关目的地的嵌套图
  • <include> 元素,可让您嵌入另一个导航图文件,就像事先完成了嵌套一样。

借助这种灵活的组合方式,您可以将一些较小的导航图组合在一起,形成应用的完整导航图。即便这些较小的导航图是由不同的模块提供,也没有关系。

在本主题的示例中,每个功能模块都专注于一项功能,并提供一个导航图,其中封装了实现该功能所需的所有目的地。在正式版应用中,很多较低级别的子模块可能会包含较此高级别功能模块的实现详情。每个功能模块都会直接或间接地包含到 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 添加到界面中。在 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> 

在顶级导航图中包含所需库后,您就可以根据需要导航到库图。例如,您可以创建一项操作,从导航图中的 fragment 导航到设置图,如下所示:

<?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. 图中定义的目的地

app 模块中的顶级导航

Navigation 组件包含 NavigationUI 类。此类包含多种静态方法,可帮助您使用顶部应用栏、抽屉式导航栏和底部导航栏来管理导航。如果应用的顶级目的地由功能模块提供的界面元素组成,那么顶级导航元素和界面元素自然而然就会放到 app 模块中。由于 app 模块依赖于协作功能模块,因此后者所有的目的地都可以通过在 app 模块中定义的代码进行访问。也就是说,如果项的 ID 与目的地的 ID 匹配,您就可以使用 NavigationUI 将目的地关联到菜单项

在图 2 中,示例 app 模块在其主 activity 中定义了 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 处理底部导航,请在主 activity 类的 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 就会导航到相应的库图。

请注意,一般情况下,应避免让 app 模块硬依赖于在功能模块的导航图中深度嵌入的特定目的地。在大多数情况下,最好让 app 模块只知道嵌入或包含的导航图的入口点(这种做法也适用于功能模块以外的情况)。如果您需要链接到库导航图深层的某个目的地,首选方式是使用深层链接。深层链接也是一个库导航到另一个库导航图中目的地的唯一方式。

跨功能模块导航

在编译时,独立的功能模块彼此看不到对方,因此您无法使用 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> 

然后,在主屏幕 fragment 中,向按钮的 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 或目的地 ID 的导航不同,您可以导航到任意图表的任意 URI,甚至可以跨模块导航。

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