前语:人总是天经地义的忘掉,是谁风里雨里,一直默默的守护在原地。
前语
Navigation
作为 Android Jetpack 组件库中的一员,是一个通用的页面导航结构。为单 Activity 架构而生的端内路由导航,用来办理 Fragment
的切换,而且能够经过可视化的方式,看见 App 的交互流程。今日首要来剖析 Navigation
的简单用法和内部原理。
Navigation
是 Jetpack 组件库很多优秀组件之一,它的定位是页面路由导航,有以下几点优势:
- 支撑 Activity,Fragmegnt,Dialog 跳转;
- 支撑跳转时数据的安全性,safeArgs 安全数据传递;
- 自界说拓展
Navigation
; - 支撑深度链接 Deeplink,Deeplink 供给了页面直达的才能;
- 支撑可视化修改,与 Android studio 绑定,供给了可视化修改界面;
- 回退仓库办理,支撑逐一出栈,也支撑回到某个页面。
一、基本运用
- 在
build.gradle
文件中增加依靠,目前版本是2.5.3
implementation 'androidx.navigation:navigation-fragment:$version'
implementation 'androidx.navigation:navigation-ui:$version'
- 在 res 文件夹下新建一个 navigaton 文件夹,创立导航图
mobile_navigation.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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/nav_main">
<fragment
android:id="@+id/nav_main"
android:name="com.sum.navigation.MainFragment"
android:label="HomeNavFragment"
tools:layout="@layout/fragment_home_nav" />
<activity
android:id="@+id/nav_activity"
android:name="com.sum.navigation.NavActivity"
android:label="NavActivity"
tools:layout="@layout/activity_nav" />
<fragment
android:id="@+id/nav_fragment"
android:name="com.sum.navigation.NavFragment"
android:label="FindNavFragment"
tools:layout="@layout/fragment_home_nav" />
<dialog
android:id="@+id/nav_dialog"
android:name="com.sum.navigation.NavDialog"
android:label="NavActivity"
tools:layout="@layout/activity_nav" />
</navigation>
该文件中包含着一切节点(意图地),能够在里边指定 Activity,Fragment,Dialog 节点。
- 在 MainActivity 中的 xml 文件中增加宿主容器:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
<fragment
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/mobile_navigation" />
</FrameLayout>
-
app:defaultNavHost="true"
:点击回来键的时分主动阻拦回来按键,履行页面出栈的操作。 -
app:navGraph="@navigation/mobile_navigation"
:指定一个 navigation 资源文件,app 中的一切节点全在这个文件傍边。 -
app:startDestination
:表明mobile_navigation
资源文件加载完结之后第一次显现的页面,这儿是先显现 MainFragment。
内容区运用的是 Fragment 来承载,而且指定了别号 androidx.navigation.fragment.NavHostFragment
,它便是一个宿主 Fragment,也便是说主页面的 Fragment 以及其他页面节点(意图地)都将嵌套在 NavHostFragment
下面。在运用的时分必需求经过 navGraph 特点把它和 NavHostFragment
相相关。
- 进入 MainActivity 首要加载的是 MainFragment:
class MainFragment : Fragment() {
private lateinit var binding: FragmentHomeNavBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentHomeNavBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//寻找出控制器目标,它是导航跳转的仅有进口
val navController = findNavController()
binding.tvNavActivity.setOnClickListener {
//导航到Activity(意图地)
navController.navigate(R.id.nav_activity)
}
binding.tvNavFragment.setOnClickListener {
// 导航到Fragment
navController.navigate(R.id.nav_fragment)
}
binding.tvNavDialog.setOnClickListener {
// 导航到dialog
navController.navigate(R.id.nav_dialog)
}
}
}
在运用 Navigation
的时分,有必要将一切的节点(意图地)增加到 res 文件的 navigation 目录下面的 mobile_navigation.xml
资源文件傍边,然后需求在 activity_main.xml
中经过 navGraph 把mobile_navigation.xml
和 NavHostFragment
相关起来,这才会让宿主把咱们界说的节点加载出来。
-
NavController
是履行Navigation
跳转是仅有的进口,经过 navigate 完结导航跳转,可带着参数,指定转场动画。它有多个重载办法:
val navController: NavController
//点击履行navigate办法跳转到对应id的节点,能够指定Bundle参数。
navController.navigate(int resId, Bundle args, NavOptions navOptions)
//点击履行navigate办法,会去mobile_navigation.xml中找是否有对应的uri,假如有就会把跳转到该节点上。
navController.navigate(uri Uri)
- deepLink 完结页面直达才能
navController.handleDeepLink(Intent())
能够指定一个 uri,当以 uri 的形式来跳转的时分,这个 navigation 会主动从界说的节点傍边哪个节点契合条件传递进来的 uri,然后去发动它。
- 办理 Fragment 回退栈
navController.navigateUp() //回退到上一个页面
navController.popBackStack(int destinationId, boolean inclusive)
- 在节点下面能够增加界说的特点:
-
<action>
:界说导航的行为; -
<argument>
:导航节点被创立的时分所需求的参数以及参数的类型; -
<deepLink>
:能够指定一个 uri,当以 uri 的形式来跳转的时分,这个 navigation 会主动从界说的节点傍边哪个节点契合传递进来的 uri,然后去发动它。
这是在 mobile_navigation.xml
资源文件傍边,去界说一些其他的特点,还有非常多这儿不一一列举了。其实这些现已和Android studio 相绑定,点击 Design 进入可视化修改即可增加,见顶部大图的右侧栏。
另外,假如需求完结主页 tab 栏作用,则需求运用 BottomNavigationView
相关起来,一同还需求指定一个 menu
特点,里边界说是按钮 item。具体运用可参阅我的 jetpack 实战开源项目:github.com/suming77/Su…
二、Navgation架构概述
导航组件由三个关键部分组成:
-
导航图: 即
mobile_navigation.xml
,在一个会集方位包含一切导航相关信息的 XML 资源。这包含运用内一切单个内容区域(称为方针)以及用户能够经过运用获取的或许途径。 -
NavHost: 显现导航图中方针的空白容器,表明一切节点的宿主。导航组件包含一个默许
NavHost
完结NavHostFragment
),它会显现导航图中的不同意图地。 -
NavController:导航控制器,它有着承上启下的作用,会将导航行为委托给它,会经过咱们传递的导航视图文件去解析,解析完结之后,就会生成
NavGraph
目标。
-
NavgationProvider:导航器
Navgator
办理者,经过它抵达每个意图地,实践上便是一个 HashMap。 - Navigator: 导航器,能够实例化对应的NavDestination,能指定导航,能回退导航。
- NavGraph: 它里边存储了一切的导航节点(意图地),也便是存储了一切的页面信息。
- NavDestination:意图地,表明导航节点,一个个页面。意图地是指您可在运用中导航到的任何方位,一般是 fragment 或 activity。
-
mBackStack: 回退栈办理,每次翻开一个页面都会增加一个
NavBackStackEntry
。
NavHostFragment
表明一切节点的宿主,app:navGraph
答应在 xml 文件中界说导航视图,导航视图里边就界说了一个个的导航节点。
导航时,能够经过页面的 ID 在 NavGraph
查找到方针页的节点,运用 NavController
目标,在导航图中向该目标指示您要去的地方或要运用的途径。NavController
随后会在 NavHostFragment
中显现相应的意图地。
三、原理剖析
在运用 Navgation
这个组件的时分,就会运用到 NavHostFragment
因为其他几个 Fragment 都是嵌套在里边,而且 navigation/mobile_navigation
资源文件也传递了进去。
咱们先从 NavHostFragment
开始看是怎样将 mobile_navigation
这个资源文件是怎样被解析生成 navGraph
这个目标的?页面的节点 NavgationDestination
也是怎样被创立的?在跳转的时分导航又是怎样被履行的?
1. 导航文件解析
一般在自界说 View 的结构函数里边经过 AttributeSet 来解析咱们的自界说特点。可是 NavHostFragment
的结构函数里边没有 AttributeSet 这个参数,那么界说在xml的 app:defaultNavHost
和 app:navGraph
等特点又是怎样解析的呢?
确实不是结构办法里边解析的,也不是在 Fragment 的 onCreate
办法中解析的,而是在 NavHostFragment
的 onInflate()
解析的。
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs, R.styleable.NavHost);
final int graphId = navHost.getResourceId(R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
}
这个办法的入参也会有 AttributeSet
这个参数,然后能解析在布局中界说的特点。
任何在布局文件傍边声明的组件比方 view,Fragment 当它们在布局傍边解析完结,创立成功之后都会回调到 onInflate()
这个办法,可是 Activity 和 Dialog 是没有这个办法的,因为它们还不支撑在布局傍边声明这两个组件。
当这个两个特点解析完结之后就再从宿主的 onCreate()
办法看起:
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
// 1.构建NavHostController目标
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
//2. 设置回来键的Dispatcher,当点击了回来键后将事情分发
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mNavController.setViewModelStore(getViewModelStore());
//3.经过NavigatorProvider创立Navigator
onCreateNavController(mNavController);
if (mGraphId != 0) {
// 4.设置从 onInflate() 解析得到的mGraphId
mNavController.setGraph(mGraphId);
} else {
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
这儿首要做了四件事:
- 首要构建了
NavHostController
目标; - 设置回来键的Dispatcher;
- 经过
NavigatorProvider
创立Navigator
; - 设置从
onInflate()
解析得到的mGraphId
;
构建 NavHostController
首要构建了 NavHostController
目标,实践上 NavHostController
什么都没有,而是承继自 NavController
,这样做的意图仅仅是为了和 NavHostFragment
在概念上统一,都是宿主的意思。
public NavController(@NonNull Context context) {
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
在结构函数里边它注册了两个 Navigator
:
-
ActivityNavigator
: 便是能够为 Activity 这种组件供给导航服务的Navigator
; -
NavGraphNavigator
: 它是比较特殊的,便是当mobile_navigation
这个资源文件加载完结之后用来发动startDestination
的 id 对应的主页,后面还会继续讲到。
之所以在这儿实例化注册的原因,是因为一旦确认了 ActivityNavigator
和 NavGraphNavigator
,Navigation
导航器就无法发动 Activity,一同也无法发动导航的主页,Activity 对于一个运用来说是不可或缺的,可是 Fragment 对于一切的运用来说不是有必要的。所以 Fragment 类型的 Navigator
并没有在这儿注册。那么它是在哪里注册的呢?
设置回来键的Dispatcher
经过 requireActivity().getOnBackPressedDispatcher()
得到一个 OnBackPressedDispatcher
,它的作用便是当点击了手机的回来键之后,才能够将事情分发给一个个注册进来的 callback;
@Override
public void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
super.setOnBackPressedDispatcher(dispatcher);
}
void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
// 将之前注册的移除
mOnBackPressedCallback.remove();
// 增加到dispatcher
dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
}
拿到 dispatcher
之后调用 addCallback()
将 mOnBackPressedCallback
注册进去,当点击了手机的回来键之后就会回调这个办法 handleOnBackPressed()
:
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
popBackStack();
}
};
在 popBackStack()
里边 NavGationConttroller
就能够做回退栈的相关操作了,平常假如要监听 Activity 的 onBackPressed()
的动作,能够运用 Activity 中的 OnBackPressedDispatcher
向它注册一个回调监听,当点击手机的回来键就会分发到 callback 里边了。
创立 Navigator
protected void onCreateNavController(@NonNull NavController navController) {
// 注册DialogFragmentNavigator
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
// 注册FragmentNavigator
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
经过 NavController
得到 NavigatorProvider
,它实践上是存储一个个的 Navigator
目标的,是导航到意图地的有用办法。
Fragment 宿主把 DialogFragmentNavigator
和 FragmentNavigator
注册了,而这两个 Navigator
便是支撑 DialogFragment
和 Fragment 页面跳转的,这便是为什么运用 Navigation
导航库的时分运用 NavHostFragment
的原因,否则无法发动 Fragment 的发动和跳转了。
设置 mGraphId
mNavController.setGraph(mGraphId);
把传递进来的导航文件 id 传递了进去,由它去加载这个资源文件,而且生成导航视图 NavGaph
目标。那么就适当 Fragment 宿主,把导航加载以及导航的才能,悉数委托给了 NavController
,而 NavHostFragment
并不关心导航的存在,起到了阻隔的作用。
宿主并不需求知道导航的概念,这样设计的优点便是即使换了一个宿主,只需求在一个新的宿主傍边创立一个 NavController
就能够完结导航的跳转了。那么导航资源文件怎样设置的?
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
getNavInflater().inflate(graphResId)
经过 inflate 办法解析传递进去的节点资源文件:
// 从给出的资源文件id解析出NavGraph
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
String rootElement = parser.getName();
NavDestination destination = inflate(res, parser, attrs, graphResId);
//
return (NavGraph) destination;
}
}
NavInflater
这个类便是专门用来解析导航图资源文件的,解析完结后回来一个 NavGraph
目标,其实 xml 的解析都是同一个套路,便是一个个去遍历 xml 中的标签,然后和已知的标签去做对比,然后再分门别类去收集去解析,该标签下面的特点,其实和解析自界说特点差不多。
2. 导航节点创立
敞开了一个 for 循环,调用 inflate 办法加载 NavDestination
,它便是在导航文件中的一个个节点:
private NavDestination inflate(Resources res, XmlResourceParser parser,
AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
// 1.parser读出标签的称号,然后得到一个Navigator
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
// 2.笼统办法,子类完结
final NavDestination dest = navigator.createDestination();
// 3.解析参数
dest.onInflate(mContext, attrs);
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)
) {
// 4.节点的特点
final String name = parser.getName();
if (TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId) {
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
// 5.假如第一个创立的节点是NavGraph,就会递归调用这儿的办法,递归调用回来的节点就会被增加到(NavGraph) dest里边
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
// 6.增加Destination
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
}
这儿首要做了三件事:
-
parser
依据称号获取Navigator
,创立Destination
; -
dest.onInflate()
解析参数,解析不同类型节点的特点; - 假如第一个创立的节点是
NavGraph
,递归调用回来的节点就会被增加到 dest 里边;假如是NavGraph
则直接增加Destination
。
首要parser
解读出标签的称号,而标签便是界说在 mobile_navigation.xml
中的一个个 Fragment 或许 Activity 标签和根节点 navigation
标签。
获取 Navigator 创立 Destination
每个意图地都与一个 Navigator
相相关,该导航器知道怎样导航到这个特定的意图地。得到一个 Navigator
目标,假如解析的是 Fragment 标签,那么得到的便是 FragmentNavigator
,假如解析的是 Activity 标签得到的便是 ActivityNavigator
标签,然后调用 navigator.createDestination()
办法,它是一个笼统办法,具体的完结在子类里边。这儿以 ActivityNavigator
为例:
public Destination createDestination() {
return new Destination(this);
}
Destination
承继自 NavDestination
,结构函数有两个 :
public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
this(NavigatorProvider.getNameForNavigator(navigator.getClass()));
}
public NavDestination(@NonNull String navigatorName) {
mNavigatorName = navigatorName;
}
结构函数参数不是 navigator
便是 navigatorName
,实践上无论是 Fragment 类型的节点仍是 Activty 的节点,在创立的时分都有必要把创立这个节点的 navigator
传递过来,然后让 NavDestination
持有 navigatorName
,这样做的意图是为了在跳转的时分能够依据咱们指定的方针页的ID,去找到 NavDestination
节点,进而经过 navigatorName
找到创立它的 Navigator
,才能够完结正确的跳转。这样就把 NavDestination
和创立它的 navigator 相关了起来。
dest解析参数和特点
然后经过 dest.onInflate()
解析参数:
private NavDestination inflate(res, parser, attrs, graphResId) {
//
// 3.解释参数,把AttributeSet attrs传递了进去
dest.onInflate(mContext, attrs);
//
}
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
final TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.Navigator);
setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
mIdName = getDisplayName(context, mId);
setLabel(a.getText(R.styleable.Navigator_android_label));
a.recycle();
}
它里边只解析了有必要的参数 id,这个 id 便是导航节点的 id,一般也叫做页面的 id,父类完结后就交由对应的子类去解析对应的特点,
这儿以 ActivityNavigator
为例,看它怎样解析的:
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.ActivityNavigator);
// 1.解析targetPackage
String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
if (targetPackage != null) {
targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
context.getPackageName());
}
setTargetPackage(targetPackage);
// 2.解析className
String className = a.getString(R.styleable.ActivityNavigator_android_name);
if (className != null) {
if (className.charAt(0) == '.') {
className = context.getPackageName() + className;
}
setComponentName(new ComponentName(context, className));
}
setAction(a.getString(R.styleable.ActivityNavigator_action));
// 2.解析data数据
String data = a.getString(R.styleable.ActivityNavigator_data);
if (data != null) {
setData(Uri.parse(data));
}
setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
a.recycle();
}
经过传递进来的 AttributeSet attrs
去解析一个个特点,比方 targetPackage,className,action 等,这些特点解析完结之后都会存储到 Destination
里边,在做导航的时分就能去创立而且发动它了,对于 Fragment 也是同理的。Fragment 更为简单,因为它只需求解析一个 FragmentClassName
就能够了。
在 NavInflater
的 inflate()
办法中,因为第一个节点是 navigation
(上面的xml文件中可见),所以 parse.getName()
获取到的便是 navigation
,而 getNavigator()
得到的便是 NavGraphaNavigator
,由它再去创立一个 Destination
:
//# NavGraphNavigator.class
public NavGraph createDestination() {
return new NavGraph(this);
}
NavGraph
增加 Destination
将自己传递了进去,一同让 NavGraph
持有自己的姓名,NavGraph
同样是 NavDestination
的子类,也便是说他同样是一个节点,可是它是一个特殊的节点,因为它存在一个 mStartDestId
,便是在导航傍边要发动的那个主页的 mStartDestId
,而同样在 onInflate()
傍边来解析:
//1.NavGraph也承继自NavDestination,所以说自己也能够嵌套自己的
//即在mobile_navition.xml文件中的Destination节点下嵌套Destination
public class NavGraph extends NavDestination implements Iterable<NavDestination> {
//存储NavDestination节点,
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
private int mStartDestId;//要发动的主页id
private String mStartDestIdName;
public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) {
super(navGraphNavigator);
}
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.NavGraphNavigator);
setStartDestination(
a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
mStartDestIdName = getDisplayName(context, mStartDestId);
a.recycle();
}
}
NavGraph
是一个 NavDestination
节点的集合,可经过 ID 获取。NavGraph
用作“虚拟”意图地:而 NavGraph
自身不会出现在后仓库上,导航到 NavGraph
将导致意图地将被增加到后仓库。
<navigation
android:id="@+id/mobile_navigation"
app:startDestination="@id/nav_main">
//导航节点嵌套
<navigation>
<fragment
android:id="@+id/blankFragment"
android:name="com.sum.navigation.BlankFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_blank" />
<fragment
android:id="@+id/dashboardFragment"
android:name="com.sum.navigation.DashboardFragment"
android:label="fragment_dashboard"
tools:layout="@layout/fragment_dashboard" />
</navigation>
</navigation>
那么这儿就会有导航组的概念,每个组都会有一个主页,经过 startDestination
来指定,那么咱们在封闭这个组的时分也就封闭了这个组里边的一切的节点。
private NavDestination inflate(res, parser, attrs, graphResId) {
//
final String name = parser.getName();
if (TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
//4.假如第一个创立的节点是NavGraph,就会递归调用这儿的办法,递归调用回来的节点就会被增加到(NavGraph) dest里边
} else if (dest instanceof NavGraph) {
// 5.增加Destination
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
实践上便是被增加到 mNodes
里边:
public final void addDestination(NavDestination node) {
//
NavDestination existingDestination = mNodes.get(node.getId());
//
node.setParent(this);
mNodes.put(node.getId(), node);
}
然后这儿的导航资源文件就解析完结了:
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
String rootElement = parser.getName();
//1.回来NavDestination
NavDestination destination = inflate(res, parser, attrs, graphResId);
//2.假如根节点不是Destination则直接抛出反常
//回来一个NavGraph
return (NavGraph) destination;
}
}
回到 NavController
,又调用了内部的 setGraph()
办法:
public void setGraph(NavGraph graph, Bundle startDestinationArgs) {
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
这儿会把刚刚解析完结的导航图资源文件而生成的 NavGraph
保存起来,然后又调用 onGraphCreated()
:
private void onGraphCreated(Bundle startDestinationArgs) {
//
if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
//发动第一个导航节点,跳转
navigate(mGraph, startDestinationArgs, null, null);
}
}
}
以上都是导航节点解析和创立的流程。如下图:
3. 三种默许类型的导航才能的完结
下面进入导航跳转的流程,在调用 navigate()
的时分把 mGraph
传递了进去:
//虽然运用NavDestination来接纳,可是传递进来的实践是NavGraph
private void navigate(NavDestination node, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
// 1.经过node.getNavigatorName()找到创立这个节点的Navigator,它实践便是NavGraph目标
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
// 2.调用navigate()建议真实的导航
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
// 3.当履行navigate()后就会把本次的节点增加到回退栈傍边
if (mBackStack.isEmpty()) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
mLifecycleOwner, mViewModel);
mBackStack.add(entry);
}
// 保证一切中心的navgraph都放在回退栈上,保证全局操作作业
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
mLifecycleOwner, mViewModel);
hierarchy.addFirst(entry);
mBackStack.addAll(hierarchy);
// 最终,运用它的默许参数增加新的方针
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
mBackStack.add(newBackStackEntry);
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null) {
dispatchOnDestinationChanged();
}
}
这儿首要做了三件事:
- 经过
node.getNavigatorName()
找到创立这个节点的Navigator
,它实践便是NavGraph
目标; - 调用
navigate()
建议真实的导航; - 当这个导航履行成功之后就会把本次的节点增加到回退栈傍边
mBackStack.add(entry)
,点击了回来键之后就会被NavController
给阻拦了下来,就能够履行真实的回退栈操作了。
进入 NavGraphNavigator#navigate()
看看是怎样将主页发动起来的:
//NavGraphNavigator
public NavDestination navigate(NavGraph destination, Bundle args,
NavOptions navOptions, Extras navigatorExtras) {
//经过NavGraph找到Destination的id,这个便是导航傍边主页要起动的id
int startId = destination.getStartDestination();
//经过startId找到这个主页对应的NavDestination
NavDestination startDestination = destination.findNode(startId, false);
//经过NavigatorName找到创立这个节点的Navigator
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}
此时这儿的 Navigator
就有或许是 AvtivityNavgatior
、FragmentNavgatior
以及 DialogNavgatior
,也便是说这个 NavGrpahNavgatior
它自己并没有正在履行导航跳转操作,而是把跳转委托给了其他三种 Navgatior
去完结履行,这个类存在的作用便是在 mobile_navigation.xml
资源文件加载完结之后,把主页给发动起来,进入 navigate()
办法。
ActivityNavigator
//#ActivityNavigator.class
public NavDestination navigate(Destination destination, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
//
// 1.将需求传递的参数放入intent
Intent intent = new Intent(destination.getIntent());
if (args != null) {
intent.putExtras(args);
String dataPattern = destination.getDataPattern();
data.append(Uri.encode(args.get(argName).toString()));
matcher.appendTail(data);
// 用参数填充数据形式,以构建有用的URI
intent.setData(Uri.parse(data.toString()));
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
intent.addFlags(extras.getFlags());
}
// 2.对恳求形式进行判别
if (!(mContext instanceof Activity)) {
//假如不是从Activity上下文发动,则有必要在一个新任务中发动
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
final int destId = destination.getId();
intent.putExtra(EXTRA_NAV_CURRENT, destId);
// 3.翻开和退出设置动画作用
if (navOptions != null) {
// For use in applyPopAnimationsToPendingTransition()
intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
}
// 4.最终经过startActivity()来完结Activity类型的导航节点跳转的才能
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
//
return null;
}
ActivityNavigator
的 navigate()
将需求传递的参数放入 intent,增加 Data 数据;对恳求形式进行判别,设置 flag;设置翻开和退出动画作用;最终经过 startActivity()
来完结 Activity 类型的节点的导航才能。
FragmentNavgator
FragmentNavgator
的 navigate()
的完结:
//#FragmentNavgator.class
public NavDestination navigate(Destination destination, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
// 1.经过传递进来的destination得到ClassName,也便是Fragment的全类名
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
// 2.依据ClassName反射出一个Fragment目标
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
// 3.设置参数并敞开业务
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
// 设置进场进场动画作用
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
// 4.经过`replace()`将Fragment增加到容器上面
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
boolean isAdded;
// 5.增加到撤退栈,假如Fragment现已存在后栈中,则替换掉
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
ft.setReorderingAllowed(true);
// 6.提交业务
ft.commit();
// 提交成功更新视图
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
使用传递进来的 destination 得到 ClassName,反射出一个 Fragment 目标,设置参数而且敞开业务,设置进进场动画等, 经过 replace()
将 Fragment
增加到容器上面,一同更新在撤退栈中,提交业务。
Fragment 在来回切换的时分都会被频繁销毁重建,从头履行他们的生命周期,假如要防止这种情况能够自定一个 FragmentNavgator
,重写 navigate()
办法,运用 hide()
和 show()
完结(开源项目中已解决这个问题)。这便是 FragmentNavgator
完结 Fragment 完结导航节点页面的剖析。
DialogFragmentNavigator
下面看下 DialogFragmentNavigator
的 navigate()
:
//#DIalogFragmentNavigator.calss
public NavDestination navigate(Destination destination, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
//1.经过Destination节点得到ClassName,也便是DialogFragment的全类名
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
//2.经过反射构建一个Fragment目标
final Fragment frag = mFragmentManager.getFragmentFactory().instantiate(
mContext.getClassLoader(), className);
//判别是否为DialogFragment类型
if (!DialogFragment.class.isAssignableFrom(frag.getClass())) {
throw new IllegalArgumentException("Dialog destination " + destination.getClassName()
+ " is not an instance of DialogFragment");
}
//3.强转而且设置参数和Observer
final DialogFragment dialogFragment = (DialogFragment) frag;
dialogFragment.setArguments(args);
dialogFragment.getLifecycle().addObserver(mObserver);
//4.显现Dialog
dialogFragment.show(mFragmentManager, DIALOG_TAG + mDialogCount++);
return destination;
}
经过 className 反射创立 DialogFragment
,强转而且设置参数和 Observer,最终经过调用 show()
显现 Dialog。
四、总结
剖析到这儿,Navigation
导航库是怎样解析导航图文件,以及节点怎样被创立,Activity,Fragment,DialogFragment 三种默许类型的导航才能是怎样被完结的,相信你现已找到了答案。流程图如下:
-
首要需求一个承载页面的容器 NavHost,这个容器有个默许的完结 NavHostFragment,app:navGraph 加载导航图 xml;
-
NavHostFragment 有个 NavController 目标,页面导航都是经过调用它的 navigate 办法完结跳转的;
-
NavController 经过调用 setGraph() 办法,传入导航资源文件,经过 NavInflater 解析导航资源文件,获取导航资源文件中的节点以及特点,得到 NavGraph;
-
NavController 内部经过 NavigatorProvider 办理这几种 navigator;
-
NavController 内经过 mBackStack 办理回退栈,设置回来键的 Dispatcher 监听,popBackStack() 就能够做回退栈的相关操作;
-
NavHostFragment 在 oncreate 办法中,NavController 增加了四个 navigator,别离是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,别离完结各自的 navigate 办法,进行页面切换。
-
在 navigate 办法中,经过设置参数,action,动画等数据后,依据原生方式完结跳转指定页面,一同会把本次的节点增加到回退栈傍边。
优点:
- 给 Activity,Fragment,Dialog 供给导航才能的组件。
- 导航时可带着参数,指定转场动画。
- 支撑deepline页面直达,fragment回退栈办理才能。
缺点:
- 非常依靠XML文件,一切的节点都必需求在
mobile_navigation.xml
文件中来界说,这是不行灵活,不利于模块化,组件化开发。 - Fragment 类型的节点来履行导航的时分运用的
replace()
办法会导致页面从头加载重走生命周期办法,不行友好。 - 不支撑导航进程的阻拦和监听。
这是从零到一建立一个组件化 + 模块化 + 协程 + Flow + Jetpack + MVVM
的App,项目地址:github.com/suming77/Su…
点关注,不迷路
好了各位,以上便是这篇文章的悉数内容了,很感谢您阅览这篇文章。我是suming,感谢各位的支撑和认可,您的点赞便是我创作的最大动力。山水有相逢,咱们下篇文章见!
本人水平有限,文章难免会有过错,请批评指正,不胜感激 !
参阅链接:
- Navigation官网
- Jetpack 导航