Navigation Component 多模块导航与深度链接实战
在 Android 单 Activity 架构中,Navigation Component 是官方推荐的导航方案。但在实际项目中,我们往往面临多模块协作、深度链接跳转、类型安全传参等复杂场景。本文将结合实战经验,讲解如何在多模块项目中组织导航图、配置深度链接,以及与底部导航栏集成的最佳实践。
一、多模块项目中的导航图设计
1.1 模块化导航图拆分
在多模块项目中,每个功能模块应该维护自己的导航图,主模块负责组合这些子图。
feature-home 模块的导航图 (navigation/nav_home.xml):
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_home"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.feature.home.HomeFragment"
android:label="首页">
<action
android:id="@+id/action_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.feature.home.DetailFragment"
android:label="详情">
<argument
android:name="itemId"
app:argType="string" />
</fragment>
</navigation>
主模块组合导航图 (navigation/nav_main.xml):
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_main"
app:startDestination="@id/nav_home">
<include app:graph="@navigation/nav_home" />
<include app:graph="@navigation/nav_profile" />
<include app:graph="@navigation/nav_settings" />
<!-- 全局导航,可从任意页面跳转 -->
<fragment
android:id="@+id/loginFragment"
android:name="com.example.app.LoginFragment"
android:label="登录" />
</navigation>
1.2 跨模块导航的实现
使用 NavController 的 navigate() 方法,通过 action 或 destination ID 跨模块跳转:
kotlin
// 从 Home 模块跳转到 Profile 模块
findNavController().navigate(R.id.nav_profile)
// 带参数跨模块跳转
val bundle = bundleOf("userId" to "12345")
findNavController().navigate(R.id.profileDetailFragment, bundle)
注意事项:
- 跨模块导航需要在主模块中声明目标 Fragment 的 ID
- 建议在
core模块中定义统一的NavigationIds常量,避免硬编码
二、深度链接(DeepLink)配置与验证
2.1 静态深度链接配置
在导航图中为 Fragment 配置 <deepLink>:
xml
<fragment
android:id="@+id/detailFragment"
android:name="com.example.feature.home.DetailFragment"
android:label="详情">
<argument
android:name="itemId"
app:argType="string" />
<deepLink
android:id="@+id/deeplink_detail"
app:uri="myapp://home/detail/{itemId}" />
<!-- 支持 HTTP 链接 -->
<deepLink
app:uri="https://www.example.com/detail/{itemId}" />
</fragment>
在 AndroidManifest.xml 中声明 <nav-graph>(自动生成 intent-filter):
xml
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 让 Navigation 自动处理深度链接 -->
<nav-graph android:value="@navigation/nav_main" />
</activity>
2.2 动态深度链接创建
在运行时生成可分享的链接:
kotlin
val deepLinkRequest = NavDeepLinkRequest.Builder
.fromUri("myapp://home/detail/item123".toUri())
.build()
findNavController().navigate(deepLinkRequest)
生成 PendingIntent 用于通知跳转:
kotlin
val args = bundleOf("itemId" to "item123")
val pendingIntent = findNavController().createDeepLink()
.setDestination(R.id.detailFragment)
.setArguments(args)
.createPendingIntent()
// 用于通知
val notification = NotificationCompat.Builder(context, channelId)
.setContentTitle("新消息")
.setContentText("查看详情")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.build()
2.3 深度链接验证
使用 adb 命令测试深度链接:
bash
# 测试自定义 scheme
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://home/detail/item123" com.example.app
# 测试 HTTP 链接
adb shell am start -W -a android.intent.action.VIEW \
-d "https://www.example.com/detail/item123" com.example.app
在代码中获取深度链接参数:
kotlin
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val itemId = args.itemId
loadDetail(itemId)
}
}
三、SafeArgs 类型安全传参
3.1 启用 SafeArgs 插件
在模块的 build.gradle.kts 中:
kotlin
plugins {
id("androidx.navigation.safeargs.kotlin")
}
3.2 定义参数类型
在导航图中声明参数:
xml
<fragment
android:id="@+id/detailFragment"
android:name="com.example.feature.home.DetailFragment">
<argument
android:name="itemId"
app:argType="string" />
<argument
android:name="fromSource"
app:argType="string"
android:defaultValue="unknown" />
<argument
android:name="isVip"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
3.3 类型安全导航
SafeArgs 会自动生成 Directions 和 Args 类:
kotlin
// 发起导航(类型安全)
val action = HomeFragmentDirections.actionToDetail(
itemId = "item123",
fromSource = "home_banner",
isVip = true
)
findNavController().navigate(action)
// 接收参数(类型安全)
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val itemId: String = args.itemId
val fromSource: String = args.fromSource
val isVip: Boolean = args.isVip
}
}
优势:
- 编译期类型检查,避免参数类型错误
- 自动生成代码,减少手动 Bundle 操作
- 支持默认值,简化可选参数传递
四、底部导航栏与 NavController 集成
4.1 BottomNavigationView 集成
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNav.setupWithNavController(navController)
}
}
布局文件(activity_main.xml):
xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
底部导航菜单(menu/bottom_nav_menu.xml):
xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/nav_home"
android:icon="@drawable/ic_home"
android:title="首页" />
<item
android:id="@id/nav_profile"
android:icon="@drawable/ic_profile"
android:title="我的" />
<item
android:id="@id/nav_settings"
android:icon="@drawable/ic_settings"
android:title="设置" />
</menu>
关键点:
menu中的item id必须与导航图中的destination id一致setupWithNavController()会自动处理选中状态与返回栈
4.2 控制底部导航栏的显示与隐藏
某些页面(如详情页、登录页)不需要显示底部导航栏:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var bottomNav: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
bottomNav = findViewById(R.id.bottom_nav)
// 监听导航变化,动态显示/隐藏底部栏
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.nav_home, R.id.nav_profile, R.id.nav_settings -> {
bottomNav.isVisible = true
}
else -> {
bottomNav.isVisible = false
}
}
}
bottomNav.setupWithNavController(navController)
}
}
五、实战经验与踩坑记录
5.1 返回栈管理
底部导航栏切换时,默认会保留各自的返回栈。如果希望切换时清空返回栈,使用 popUpTo:
xml
<action
android:id="@+id/action_to_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_main"
app:popUpToInclusive="false" />
在代码中动态清空返回栈:
kotlin
navController.navigate(R.id.nav_home) {
popUpTo(R.id.nav_main) {
inclusive = false
}
launchSingleTop = true
}
5.2 全局导航与模块解耦
在多模块项目中,避免直接依赖其他模块的 Fragment。推荐使用"路由表"模式:
kotlin
// core 模块定义路由接口
interface Router {
fun navigateToDetail(navController: NavController, itemId: String)
}
// feature-home 模块实现路由
class HomeRouter : Router {
override fun navigateToDetail(navController: NavController, itemId: String) {
val action = HomeFragmentDirections.actionToDetail(itemId)
navController.navigate(action)
}
}
// 主模块注册路由
class App : Application() {
companion object {
lateinit var router: Router
}
override fun onCreate() {
super.onCreate()
router = HomeRouter()
}
}
// 使用路由跳转
App.router.navigateToDetail(findNavController(), "item123")
5.3 深度链接参数编码问题
URL 中的特殊字符需要编码,否则解析失败:
kotlin
// 错误:包含特殊字符
val uri = "myapp://search?q=Android开发"
// 正确:编码参数
val query = URLEncoder.encode("Android开发", "UTF-8")
val uri = "myapp://search?q=$query"
5.4 多 NavHost 场景
某些场景需要嵌套 NavHostFragment(如主页面中嵌入子导航),需要明确指定 NavController:
kotlin
// 获取父 NavController
val parentNavController = findNavController()
// 获取子 NavController
val childNavHostFragment = childFragmentManager
.findFragmentById(R.id.child_nav_host) as NavHostFragment
val childNavController = childNavHostFragment.navController
六、总结
Navigation Component 在多模块项目中的最佳实践:
- 模块化导航图:每个功能模块维护独立导航图,主模块负责组合
- 深度链接配置:支持自定义 scheme 和 HTTP 链接,便于外部唤起与内部跳转
- SafeArgs 类型安全:编译期检查参数类型,减少运行时错误
- 底部导航集成 :使用
setupWithNavController()自动管理选中状态与返回栈 - 路由表解耦:通过路由接口隔离模块依赖,提升可测试性
Navigation Component 不仅简化了导航逻辑,还提供了深度链接、类型安全传参等企业级能力。在实际项目中,合理设计导航图结构、结合路由表模式,能够显著提升多模块协作的开发效率与代码质量。