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 操作
  • 支持默认值,简化可选参数传递

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"

某些场景需要嵌套 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 在多模块项目中的最佳实践:

  1. 模块化导航图:每个功能模块维护独立导航图,主模块负责组合
  2. 深度链接配置:支持自定义 scheme 和 HTTP 链接,便于外部唤起与内部跳转
  3. SafeArgs 类型安全:编译期检查参数类型,减少运行时错误
  4. 底部导航集成 :使用 setupWithNavController() 自动管理选中状态与返回栈
  5. 路由表解耦:通过路由接口隔离模块依赖,提升可测试性

Navigation Component 不仅简化了导航逻辑,还提供了深度链接、类型安全传参等企业级能力。在实际项目中,合理设计导航图结构、结合路由表模式,能够显著提升多模块协作的开发效率与代码质量。

相关推荐
快乐非自愿2 小时前
低代码落地费控系统:从报销流程到业财闭环的实操方案
android·低代码·rxjava·ai低代码
摇滚侠3 小时前
《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3
android·java·笔记
YB13753 小时前
Jetpack Compose 动画updateTransition
android
用户2018792831673 小时前
给主厨装一台打卡机:一文搞懂 Android APM 性能监控原理
android
我不会插花弄玉4 小时前
8. 复合查询【由浅入深-MySQL】
android·数据库·mysql
熊文豪4 小时前
机器人开发者生态分析:行业分工变化与启元的定位
android·机器人·启元机器人
郑州光合科技余经理5 小时前
同城商城源码:商品库和订单库怎么随包交付
android·java·开发语言·后端·uni-app·php·ai编程
蒸鱼Yuzheng5 小时前
移动游戏功耗与能效归因:帧率、画质、温控和续航如何一起评估
android·性能测试·功耗优化·游戏性能·温控