JetPack Navigation 介绍
Jetpack Navigation 是啥
Jetpack Navigation 是 Jetpack 里面的一个依赖库,用来对Fragment 页面的导航和管理的。
使用
1 引用
// Kotlin
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
2 创建Navigation 导航文件
- 1 在 res 下面创建navigation 目录
- 2 右键navigation 创建 xml 文件 nav_graph(名字随便起)
-
3 如图所示点击创建 一个 destination
图1 创建destination1
图 2 创建destination2
按照提示添加Fragment。
-
4 依次同样的方法添加多个fragment :HomeFragment,LoginFragmrnt,UserFragment,RegisterFragment,ContentFragment,如下图是自动生成。
图 3 fragments -
5 给这些fragment 增加关系。
增加下面的线之后左边就会自动增加Action的标签。线1 增加的时候左边对应就会增加一个action,
线2 画出来的时候左边就对应增加一个2的action。
图4 action
右边的线都是拖拽就可以的,现在左边的代码还都是自动生成的。
3把navigation 图展示出来
activity 的xml 把navigation 加入进来,下面是mainactivity 的布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/nav_host_fragment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
android:layout_width="0dp"
android:layout_height="0dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
FragmentContainerView 是用来展示navigation 的布局。
注意 name字段 、 navGraph 字段和defaultNavHost 字段。
name 字段是加载的fragment 里面用来处理 destination 的逻辑跳转的容器。
navGraph 字段是用来 指示navigation。
defaultNavHost 字段用来表示 返回键单独处理不,默认单独处理走fragment的栈的地方。
这样MainActivity 就展示的是nav_graph 的startDestination 也就是homeFragment。
4 navigation 之间的跳转
下面这一行就是最简单的跳转方法
view.findViewById<Button>(R.id.btn_login).setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_loginFragment)
}
findNavController() 是Fragment 的拓展方法。
package androidx.navigation.fragment
public fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)
所有的对fragment的管理都是通过NavController 管理的。
返回: findNavController().popBackStack() 或者 findNavController().navigateUp()
// findNavController().popBackStack() 可以越级back
// findNavController().navigateUp() 在一定的条件下可以finish activity
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.btn_back).setOnClickListener {
// findNavController().popBackStack()
findNavController().navigateUp()
}
}
5 NavController
NavController 是核心类
navigate 方法
1 按照 action 跳转
这种方法是根据actionId 进行跳转官方推荐,网上文章大部分都是这样的使用。
如图4 里面fragment的里面有 action ,根据action Id 进行跳转。
findNavController().navigate(R.id.action_homeFragment_to_loginFragment)。
2 按照deepLink 跳转
使用较少,网上介绍的也比较少,但是个人推荐这种方法。
- 1 像上面图三 一样添加fragment
- 2 不给fragment 添加关系给他们手动添加 deepLink
<fragment
android:id="@+id/DFragment1"
android:name="com.eswincomputing.navigationdemo.deeplink.DFragment1"
android:label="fragment_d1"
tools:layout="@layout/fragment_d1" >
<deepLink
android:id="@+id/deep_link_d1"
app:uri="android-app://androidx.navigation/d1"/>
</fragment>
注意 uri 里面 android-app://androidx.navigation/ 这块的字符串要写固定。
用法
findNavController().navigate("d1")
findNavController().navigate("d1")
....
@JvmOverloads
public fun navigate(
route: String,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
navigate(
NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(), navOptions,
navigatorExtras
)
}
···
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun createRoute(route: String?): String =
if (route != null) "android-app://androidx.navigation/$route" else ""
3 navigate 方法参数
- 参数 1 action 和 deepLink 只有第一个参数不对其他的参数都是一样的,
以action 为例
val navOptions = NavOptions.Builder()
.setLaunchSingleTop(true) // singleTop
.setPopUpTo(R.id.homeFragment, // id 上的fragment 弹出(默认不包含这个id)
inclusive = true, // 是否包含当前的id
saveState = true // 是否保存退出站的状态
)
.setRestoreState(true) // 获取已经保存的状态
.build()
//
findNavController().navigate(
R.id.action_homeFragment_to_loginFragment, // 参数1 actionId
Bundle().apply {
putString(ARG_PARAM1,"param_from_home")
putString(ARG_PARAM2,"param_to_login")
}, //参数2 bundle
navOptions, //参数3
FragmentNavigatorExtras() // 参数4
)
参数 2 是 bundle fragment setargument 里面的参数 给fragment 传递参数的
参数3 navOptions 主要设置栈的操作(更多的选择设置)
setLaunchSingleTop 设置 singleTop
setPopUpTo(id,inclusive ,saveState )
参数 // id 的fragment 弹出(默认不包含这个id)
参数 inclusive // 是否包含当前的id的fragment
参数 saveState // 是否保存退出站的状态
setRestoreState 获取进入的framgnet 的state(之前保存的)
1.1 findNavController().navigate(R.id.action_homeFragment_to_loginFragment)
传一个resourceId
添加一个 bundle参数传递
- 2 返回
popBackStack() 正常一个一个退出
popBackStack(
@IdRes destinationId: Int,
inclusive: Boolean,
saveState: Boolean
)
destinationId 目的地fragmentid 退出这个id 上面的fragment
inclusive 是否包含这个id 目的地id
saveState 推出的fragment 是否保存state
默认加载fragment 逻辑
FragmentContainerView constructer 方法 --》 NavHostFragment
oncreate 设置属性 和 graphID
FragmentContainerView onInflate --》 NavHostFragment 的 onInflate 获取graphID 。
NavHostFragment oncreate --》
navController!!.setGraph(graphId) --》 navInflater.inflate(graphResId) -》
NavHostController--onGraphCreated -- 》NavHostController----navigate --》 FragmentNavigator —— onLaunchSingleTop,navigateInternal ——》
关键类
NavController (NavHostController)
navigate 和pop 跳转退出FragmentContainerView
activity 里面包含navigationFragment 的View
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_deeplink"
android:layout_width="0dp"
android:layout_height="0dp"/>
注意name
android:name="androidx.navigation.fragment.NavHostFragment"
FragmentContainerView {
constructer(){
···
val containerFragment: Fragment =
fm.fragmentFactory.instantiate(context.classLoader, name)
containerFragment.onInflate(context, attrs, null)
fm.beginTransaction()
.setReorderingAllowed(true)
.add(this, containerFragment, tag)
.commitNowAllowingStateLoss()
}
···
}
根据name 设置 NavHostFragment View
- 2 NavHostFragment
注意 containerFragment.onInflate(context, attrs, null)
把 FragmentContainerView 的参数 attrs 都传递给 NavHostFragment 了
public override fun onInflate(
context: Context,
attrs: AttributeSet,
savedInstanceState: Bundle?
) {
super.onInflate(context, attrs, savedInstanceState)
context.obtainStyledAttributes(
attrs,
androidx.navigation.R.styleable.NavHost
).use { navHost ->
val graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0
)
if (graphId != 0) {
this.graphId = graphId
}
}
context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment).use { array ->
val defaultHost = array.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false)
if (defaultHost) {
defaultNavHost = true
}
}
}
- 4 NavInflater 解析 graph
@SuppressLint("ResourceType")
public fun inflate(@NavigationRes graphResId: Int): NavGraph {
val res = context.resources
val parser = res.getXml(graphResId)
val attrs = Xml.asAttributeSet(parser)
return try {
var type: Int
while (parser.next().also { type = it } != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT
) { /* Empty loop */
}
if (type != XmlPullParser.START_TAG) {
throw XmlPullParserException("No start tag found")
}
val rootElement = parser.name
val destination = inflate(res, parser, attrs, graphResId)
require(destination is NavGraph) {
"Root element <$rootElement> did not inflate into a NavGraph"
}
destination
} catch (e: Exception) {
throw RuntimeException(
"Exception inflating ${res.getResourceName(graphResId)} line ${parser.lineNumber}",
e
)
} finally {
parser.close()
}
}
@Throws(XmlPullParserException::class, IOException::class)
private fun inflate(
res: Resources,
parser: XmlResourceParser,
attrs: AttributeSet,
graphResId: Int
): NavDestination {
val navigator = navigatorProvider.getNavigator<Navigator<*>>(parser.name)
val dest = navigator.createDestination()
dest.onInflate(context, attrs)
val innerDepth = parser.depth + 1
var type: Int
var depth = 0
while (parser.next().also { type = it } != XmlPullParser.END_DOCUMENT &&
(parser.depth.also { depth = it } >= innerDepth || type != XmlPullParser.END_TAG)
) {
if (type != XmlPullParser.START_TAG) {
continue
}
if (depth > innerDepth) {
continue
}
val name = parser.name
if (TAG_ARGUMENT == name) {
inflateArgumentForDestination(res, dest, attrs, graphResId)
} else if (TAG_DEEP_LINK == name) {
inflateDeepLink(res, dest, attrs)
} else if (TAG_ACTION == name) {
inflateAction(res, dest, attrs, parser, graphResId)
} else if (TAG_INCLUDE == name && dest is NavGraph) {
res.obtainAttributes(attrs, androidx.navigation.R.styleable.NavInclude).use {
val id = it.getResourceId(androidx.navigation.R.styleable.NavInclude_graph, 0)
dest.addDestination(inflate(id))
}
} else if (dest is NavGraph) {
dest.addDestination(inflate(res, parser, attrs, graphResId))
}
}
return dest
}
- 3 NavHostController: NavController
NavHostFragment 里面
···
onCreate(){
···
navHostController = NavHostController(context)
···
if (graphId != 0) {
// Set from onInflate()
navHostController!!.setGraph(graphId)
}
···
}
// 设置 navigator 是fragment naavigator
···
protected open fun onCreateNavController(navController: NavController) {
navController.navigatorProvider +=
DialogFragmentNavigator(requireContext(), childFragmentManager)
navController.navigatorProvider.addNavigator(createFragmentNavigator())
}
NavHostController 里面
public open fun setGraph(@NavigationRes graphResId: Int) {
setGraph(navInflater.inflate(graphResId), null)
}
···
@MainThread
@CallSuper
public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
if (_graph != graph) {
···
_graph = graph
onGraphCreated(startDestinationArgs)
} else {
···
}
}
}
···
private fun onGraphCreated(startDestinationArgs: Bundle?) {
···
// Navigate to the first destination in the graph
// if we haven't deep linked to a destination
navigate(_graph!!, startDestinationArgs, null, null)
···
}
@MainThread
private fun navigate(
node: NavDestination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
···
navigator.onLaunchSingleTop(newEntry) // 调用fragmentNavigator 的方法
···
navigator.navigateInternal(listOf(backStackEntry), navOptions,
navigatorExtras) {
navigated = true
addEntryToBackStack(node, finalArgs, it)
}
}
private fun Navigator<out NavDestination>.navigateInternal(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?,
handler: (backStackEntry: NavBackStackEntry) -> Unit = {}
) {
addToBackStackHandler = handler
navigate(entries, navOptions, navigatorExtras) // 调用fragmentNavigator 的方法
addToBackStackHandler = null
}
- 4 FragmentNavigator
override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
if (fragmentManager.isStateSaved) {
Log.i(
TAG,
"Ignoring onLaunchSingleTop() call: FragmentManager has already saved its state"
)
return
}
val ft = createFragmentTransaction(backStackEntry, null)
if (state.backStack.value.size > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
fragmentManager.popBackStack(
backStackEntry.id,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
ft.addToBackStack(backStackEntry.id)
}
ft.commit()
// The commit succeeded, update our view of the world
state.onLaunchSingleTop(backStackEntry)
}
private fun createFragmentTransaction(
entry: NavBackStackEntry,
navOptions: NavOptions?
): FragmentTransaction {
val destination = entry.destination as Destination
val args = entry.arguments
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
frag.arguments = args
val ft = fragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
ft.replace(containerId, frag)
ft.setPrimaryNavigationFragment(frag)
ft.setReorderingAllowed(true)
return ft
}
// NavController 调用
private fun navigate(
entry: NavBackStackEntry,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
val initialNavigation = state.backStack.value.isEmpty()
val restoreState = (
navOptions != null && !initialNavigation &&
navOptions.shouldRestoreState() &&
savedIds.remove(entry.id)
)
if (restoreState) {
// Restore back stack does all the work to restore the entry
fragmentManager.restoreBackStack(entry.id)
state.push(entry)
return
}
val ft = createFragmentTransaction(entry, navOptions)
if (!initialNavigation) {
ft.addToBackStack(entry.id)
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key, value)
}
}
ft.commit()
// The commit succeeded, update our view of the world
state.push(entry)
}