目录
- 引言
- 依赖项
- 创建导航图
- 导航宿主
- 导航到目的地
- 传递参数
- NavigationUI
- 多模块导航
引言
导航是指支持用户导航、进入和退出应用中不同内容片段的交互。Android Jetpack 的导航组件可实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。
依赖项
def nav_version = "2.5.2" | |
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" | |
implementation "androidx.navigation:navigation-ui-ktx:$nav_version" | |
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" |
创建导航图
导航发生在应用中的各个目的地之间。这些目的地是通过操作连接的,导航图是一种资源文件,其中包含所有目的地和操作,该图表会显示应用的所有导航路径。
点击加号便可以创建目的地 Fragment,连接操作由箭头表示,该箭头表示用户可以如何从一个目的地导航到另一个目的地。
目的地是指应用中的不同内容区域,操作是指目的地之间的逻辑连接,表示用户可以采取的路径。然后,我们来看下此时的导航xml代码:main_navigation.xml
<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/main_navigation" | |
app:startDestination="@id/oneFragment"> | |
<fragment | |
android:id="@+id/oneFragment" | |
android:name="com.example.myapplication.OneFragment" | |
android:label="fragment_one" | |
tools:layout="@layout/fragment_one" > | |
<action | |
android:id="@+id/action_oneFragment_to_twoFragment" | |
app:destination="@id/twoFragment" /> | |
</fragment> | |
<fragment | |
android:id="@+id/twoFragment" | |
android:name="com.example.myapplication.TwoFragment" | |
android:label="fragment_two" | |
tools:layout="@layout/fragment_two" > | |
<action | |
android:id="@+id/action_twoFragment_to_threeFragment" | |
app:destination="@id/threeFragment" /> | |
</fragment> | |
<fragment | |
android:id="@+id/threeFragment" | |
android:name="com.example.myapplication.ThreeFragment" | |
android:label="fragment_three" | |
tools:layout="@layout/fragment_three" /> | |
</navigation> |
其中,navigation 元素是导航图的根元素,向图表添加目的地和连接操作,即 action 和 destination
导航宿主
导航宿主是一个空容器,用户在应用中导航时,目的地会在该容器中交换进出。导航宿主必须派生于 NavHost,Navigation 组件的默认 NavHost 实现 NavHostFragment,负责处理 fragment 目的地的交换。
<LinearLayout 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:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<androidx.fragment.app.FragmentContainerView | |
android:id="@+id/container" | |
android:name="androidx.navigation.fragment.NavHostFragment" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
app:defaultNavHost="true" | |
app:navGraph="@navigation/main_navigation" /> | |
</LinearLayout> |
- android:name:NavHost 实现类的名称
- app:navGraph:将 NavHostFragment 与导航图相关联
- app:defaultNavHost="true":确保 NavHostFragment 会拦截系统返回按钮
注意:只能有一个默认 NavHost,如果同一布局(例如,双窗格布局)中有多个托管容器,请务必仅指定一个默认 NavHost
导航到目的地
跳转建议使用 Safe Args 确保类型安全,在 Project 下的 build.gradle 添加。启用 Safe Args 后,该插件会生成代码,其中包含每个操作的类和方法。对于每个操作,Safe Args 还会为每个源头生成一个类。生成的类的名称由源类的名称和“Directions”一词组成,例如OneFragment,生成的就是OneFragmentDirections
buildscript { | |
repositories { | |
google() | |
} | |
dependencies { | |
def nav_version = "2.5.2" | |
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" | |
} | |
} |
在APP模块的 build.gradle 添加
plugins { | |
id 'androidx.navigation.safeargs' | |
} |
点击按钮,进行跳转
jump.setOnClickListener { | |
val action = OneFragmentDirections.actionOneFragmentToTwoFragment() | |
it.findNavController().navigate(action) | |
} |
传递参数
启用 Safe Args 后,会创建一个类,该类的名称是在目的地的名称后面加上“Args”,例如 OneFragment,名称为 OneFragmentArgs,用于参数传递。 在 main_navigation.xml 加上参数标签 argument
<fragment | |
android:id="@+id/oneFragment" | |
android:name="com.example.myapplication.OneFragment" | |
android:label="fragment_one" | |
tools:layout="@layout/fragment_one"> | |
<argument | |
android:name="name" | |
android:defaultValue="1" | |
app:argType="string" /> | |
<action | |
android:id="@+id/action_oneFragment_to_twoFragment" | |
app:destination="@id/twoFragment" /> | |
</fragment> |
在 OneFragment 中进行跳转,参数传递
jump.setOnClickListener { | |
val param = OneFragmentArgs.Builder().setName(nameStr).build().toBundle() | |
it.findNavController().navigate(R.id.action_oneFragment_to_twoFragment, param) | |
} |
TwoFragment接收参数,建议使用 ktx,就可以使用 by navArgs() 属性委托来访问参数
class TwoFragment : Fragment() { | |
private val args: OneFragmentArgs by navArgs() | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
val nameStr = args.name //接收的值 | |
return inflater.inflate(R.layout.fragment_two, container, false) | |
} | |
} |
NavigationUI
此类包含多种静态方法,可帮助我们使用顶部应用栏、抽屉式导航栏和底部导航栏来管理导航。 这里简单实现一个底部导航栏,先创建一个 menu
<menu xmlns:android="http://schemas.android.com/apk/res/android"> | |
<item | |
android:id="@+id/oneFragment" | |
android:icon="@android:drawable/ic_menu_camera" | |
android:title="相机" /> | |
<item | |
android:id="@+id/twoFragment" | |
android:icon="@android:drawable/ic_dialog_email" | |
android:title="邮件" /> | |
<item | |
android:id="@+id/threeFragment" | |
android:icon="@android:drawable/ic_menu_call" | |
android:title="电话" /> | |
</menu> |
创建三个Fragment,OneFragment,TwoFragment 和 ThreeFragment,作为三个页面, 需要注意的是 menu 各个 item 的 id 和 fragment 的 id 需要相对应
<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/main_navigation" | |
app:startDestination="@id/oneFragment"> | |
<fragment | |
android:id="@+id/oneFragment" | |
android:name="com.example.myapplication.OneFragment" | |
android:label="fragment_one" | |
tools:layout="@layout/fragment_one" /> | |
<fragment | |
android:id="@+id/twoFragment" | |
android:name="com.example.myapplication.TwoFragment" | |
android:label="fragment_two" | |
tools:layout="@layout/fragment_two" /> | |
<fragment | |
android:id="@+id/threeFragment" | |
android:name="com.example.myapplication.ThreeFragment" | |
android:label="fragment_three" | |
tools:layout="@layout/fragment_three" /> | |
</navigation> |
宿主的布局如下
<LinearLayout 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:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
tools:context=".MainActivity"> | |
<androidx.fragment.app.FragmentContainerView | |
android:id="@+id/container" | |
android:name="androidx.navigation.fragment.NavHostFragment" | |
android:layout_width="match_parent" | |
android:layout_height="0dp" | |
android:layout_weight="1" | |
app:defaultNavHost="true" | |
app:navGraph="@navigation/main_navigation" /> | |
<com.google.android.material.bottomnavigation.BottomNavigationView | |
android:id="@+id/bottom" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:menu="@menu/main_item" /> | |
</LinearLayout> |
然后在宿主Activity中执行如下就行啦
val navHostFragment = | |
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment | |
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom) | |
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.navController) |
多模块导航
对于比较大型的项目,我们一般都会采用组件化开发,每个功能模块都专注于一项功能。每个功能模块都是一个独立的单元,拥有自己的导航图和目的地,app 模块依赖于每个功能模块。
app 模块负责提供应用的完整导航图,以及将 NavHost 添加到界面中,可以使用 include 来引用库图。
<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/main_navigation" | |
app:startDestination="@id/mainFragment"> | |
<include app:graph="@navigation/bike_nav" /> | |
<include app:graph="@navigation/car_nav" /> | |
<fragment | |
android:id="@+id/mainFragment" | |
android:name="com.example.myapplication.MainFragment" | |
android:label="fragment_main" | |
tools:layout="@layout/fragment_main"> | |
<action | |
android:id="@+id/action_mainFragment_to_car_nav" | |
app:destination="@id/car_nav" /> | |
<action | |
android:id="@+id/action_mainFragment_to_bike_nav" | |
app:destination="@id/bike_nav" /> | |
</fragment> | |
</navigation> |
如下图所示,这里主页面一个 MainFragment,用来做跳转,分别可跳转到两个不同的模块
然后在 MainFragment 里执行跳转逻辑
findViewById<TextView>(R.id.red).setOnClickListener { | |
it.findNavController().navigate(MainFragmentDirections.actionMainFragmentToCarNav()) | |
} | |
findViewById<TextView>(R.id.purple).setOnClickListener { | |
it.findNavController() | |
.navigate(MainFragmentDirections.actionMainFragmentToBikeNav()) | |
} |
那么,问题来了,如果是两个单独的功能模块之间需要导航呢,而独立的功能模块彼此又看不到对方,怎么办呢? 这时,可以使用深层链接直接导航到与隐式深层链接关联的目的地。
举个例子,现在要从 Bikelibrary 里的 Fragment 跳转到 Carlibrary 里的 Fragment
将 Carlibrary 中的导航添加深层链接
<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/car_nav" | |
app:startDestination="@id/carFragment"> | |
<fragment | |
android:id="@+id/carFragment" | |
android:name="com.example.carlibrary.CarFragment" | |
android:label="fragment_car" | |
tools:layout="@layout/fragment_car"> | |
<deepLink | |
android:id="@+id/deepLink" | |
app:uri="android-app://com.example.carlibrary/carFragment" /> | |
</fragment> | |
</navigation> |
当然,你也可以从xml编辑器的右侧添加
然后在 bikelibrary 里的 fragment 中根据此链接进行导航,这里设置了一个按钮的点击事件
findViewById<Button>(R.id.bike_to_car).setOnClickListener { | |
val request = | |
NavDeepLinkRequest.Builder.fromUri("android-app://com.example.carlibrary/carFragment".toUri()) | |
.build() | |
it.findNavController().navigate(request) | |
} |
这样,就完成了跨模块导航,是不是很方便呢?不过,需要注意的是,Safe Args 不支持跨模块导航,因为没有针对目的地的直接操作。