在 Android 开发中,页面导航一直是个容易出问题的环节:Fragment 回退栈混乱、参数传递不安全、深度链接配置繁琐。Jetpack Navigation Component 提供了一套声明式的导航方案,但要在真实项目中用好它,还需要处理不少工程细节。
本文从实际项目需求出发,讲解如何用 Navigation Component 搭建单 Activity 多 Fragment 架构、安全传参、处理回退栈、配置深度链接,以及在复杂场景下的边界处理。
传统导航方式的痛点:
beginTransaction() + replace() + addToBackStack()Navigation Component 的优势:
dependencies{
defnav_version="2.7.7"
implementation"androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation"androidx.navigation:navigation-ui-ktx:$nav_version"
}在项目根目录的 build.gradle 中添加:
buildscript {
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
}
}在 app 模块的 build.gradle 中应用:
plugins {
id 'androidx.navigation.safeargs.kotlin'
}在 res/navigation/ 目录下新建 nav_graph.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigationxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.HomeFragment"
android:label="首页">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"/>
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.DetailFragment"
android:label="详情">
<argument
android:name="itemId"
app:argType="string"/>
<argument
android:name="showComments"
app:argType="boolean"
app:defaultValue="false"/>
</fragment>
</navigation>在 MainActivity 的布局中添加 NavHostFragment:
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>app:defaultNavHost="true":拦截系统返回键app:navGraph:指定导航图在 HomeFragment 中:
findNavController().navigate(R.id.action_home_to_detail)Safe Args 插件会为每个声明了 <argument> 的 Fragment 生成两个类:
XXXFragmentArgs:接收参数XXXFragmentDirections:构建跳转参数发送方(HomeFragment):
valaction=HomeFragmentDirections.actionHomeToDetail(
itemId="item_123",
showComments=true
)
findNavController().navigate(action)接收方(DetailFragment):
classDetailFragment:Fragment(){
privatevalargs:DetailFragmentArgsbynavArgs()
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
super.onViewCreated(view,savedInstanceState)
valitemId=args.itemId
valshowComments=args.showComments
// 使用参数加载数据
}
}从登录页跳到首页后,不希望按返回键回到登录页:
<action
android:id="@+id/action_login_to_home"
app:destination="@id/homeFragment"
app:popUpTo="@id/nav_graph"
app:popUpToInclusive="true"/>app:popUpTo:弹出栈直到指定目的地app:popUpToInclusive="true":包括指定目的地本身也弹出从详情页返回时,跳过中间页直接回到首页:
findNavController().popBackStack(R.id.homeFragment,false)第二个参数 false 表示不包括 homeFragment 本身。
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.DetailFragment"
android:label="详情">
<argument
android:name="itemId"
app:argType="string"/>
<deepLink
app:uri="myapp://item/{itemId}"/>
</fragment><activityandroid:name=".MainActivity">
<nav-graphandroid:value="@navigation/nav_graph"/>
</activity>编译后,插件会自动生成对应的 <intent-filter>。
外部触发(如通知、浏览器):
valintent=Intent(Intent.ACTION_VIEW,Uri.parse("myapp://item/item_123"))
startActivity(intent)内部触发:
findNavController().navigate(Uri.parse("myapp://item/item_123"))在导航图根节点定义全局跳转:
<navigation...>
<action
android:id="@+id/action_global_settings"
app:destination="@id/settingsFragment"/>
<!-- 其他 fragment... -->
</navigation>任何页面都可以直接调用:
findNavController().navigate(R.id.action_global_settings)在跳转时传递共享元素:
valextras=FragmentNavigatorExtras(
imageViewto"item_image"
)
findNavController().navigate(R.id.action_home_to_detail,null,null,extras)在目标 Fragment 中设置过渡:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
sharedElementEnterTransition=TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
}findNavController().addOnDestinationChangedListener{_,destination,_->
when(destination.id){
R.id.homeFragment->toolbar.title="首页"
R.id.detailFragment->toolbar.title="详情"
}
}场景:Tab 页中每个 Tab 有自己的导航图,切换 Tab 后按返回键,回退行为不符合预期。
方案:每个 Tab 使用独立的 NavHostFragment,并在宿主 Fragment 中管理:
classTabContainerFragment:Fragment(){
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
childFragmentManager.findFragmentById(R.id.tab_nav_host)asNavHostFragment
}
}Safe Args 只支持基本类型和 Parcelable。如果需要传递复杂对象:
方案 1:传递 ID,在目标页查询
valaction=HomeFragmentDirections.actionHomeToDetail(itemId=item.id)方案 2:使用共享 ViewModel
// 在宿主 Activity 范围内共享
valsharedViewModel:SharedViewModelbyactivityViewModels()场景:URL 中传递的是字符串 "true",但 argType 是 boolean。
解决:导航图中声明 argType="string",在 Fragment 中手动转换:
valshowComments=args.showCommentsStr.toBoolean()如果项目混用了 Jetpack Compose,可以用 navigation-compose:
dependencies{
implementation"androidx.navigation:navigation-compose:2.7.7"
}在 Composable 中定义导航:
valnavController=rememberNavController()
NavHost(navController,startDestination="home"){
composable("home"){HomeScreen(navController)}
composable("detail/{itemId}"){backStackEntry->
valitemId=backStackEntry.arguments?.getString("itemId")
DetailScreen(itemId)
}
}Navigation Component 的核心价值在于把导航逻辑从代码搬到声明式配置,减少模板代码,提高可维护性。
工程落地时的关键点:
popUpTo 明确回退行为当你的 App 架构从多 Activity 迁移到单 Activity + Navigation 后,你会发现页面跳转的代码量明显减少,回退逻辑也更清晰。但也要注意,Navigation Component 不是银弹——对于特别复杂的导航场景(如多栈切换、动态路由),可能需要结合自定义 Navigator 或者干脆手动管理 Fragment。
关键是理解它的设计思想,在合适的场景用合适的工具。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。