前语:人总是天经地义的忘掉,是谁风里雨里,一直默默的守护在原地。

前语

Navigation 作为 Android Jetpack 组件库中的一员,是一个通用的页面导航结构。为单 Activity 架构而生的端内路由导航,用来办理 Fragment 的切换,而且能够经过可视化的方式,看见 App 的交互流程。今日首要来剖析 Navigation 的简单用法和内部原理。

Navigation 是 Jetpack 组件库很多优秀组件之一,它的定位是页面路由导航,有以下几点优势:

  • 支撑 Activity,Fragmegnt,Dialog 跳转;
  • 支撑跳转时数据的安全性,safeArgs 安全数据传递;
  • 自界说拓展 Navigation
  • 支撑深度链接 Deeplink,Deeplink 供给了页面直达的才能;
  • 支撑可视化修改,与 Android studio 绑定,供给了可视化修改界面;
  • 回退仓库办理,支撑逐一出栈,也支撑回到某个页面。

一、基本运用

  1. build.gradle 文件中增加依靠,目前版本是 2.5.3
implementation 'androidx.navigation:navigation-fragment:$version'
implementation 'androidx.navigation:navigation-ui:$version'
  1. 在 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 节点。

  1. 在 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 相相关。

  1. 进入 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.xmlNavHostFragment 相关起来,这才会让宿主把咱们界说的节点加载出来。

浅谈 Android 架构导航 - Navigation的架构与设计

  1. NavController 是履行 Navigation 跳转是仅有的进口,经过 navigate 完结导航跳转,可带着参数,指定转场动画。它有多个重载办法:

浅谈 Android 架构导航 - Navigation的架构与设计

val navController: NavController
//点击履行navigate办法跳转到对应id的节点,能够指定Bundle参数。
navController.navigate(int resId, Bundle args, NavOptions navOptions) 
//点击履行navigate办法,会去mobile_navigation.xml中找是否有对应的uri,假如有就会把跳转到该节点上。
navController.navigate(uri Uri)
  1. deepLink 完结页面直达才能
navController.handleDeepLink(Intent())

能够指定一个 uri,当以 uri 的形式来跳转的时分,这个 navigation 会主动从界说的节点傍边哪个节点契合条件传递进来的 uri,然后去发动它。

  1. 办理 Fragment 回退栈
navController.navigateUp() //回退到上一个页面
navController.popBackStack(int destinationId, boolean inclusive)
  1. 在节点下面能够增加界说的特点:
  • <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 目标。

浅谈 Android 架构导航 - Navigation的架构与设计

  • 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:defaultNavHostapp:navGraph 等特点又是怎样解析的呢?

确实不是结构办法里边解析的,也不是在 Fragment 的 onCreate 办法中解析的,而是在 NavHostFragmentonInflate() 解析的。

@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);
        }
    }
}

这儿首要做了四件事:

  1. 首要构建了 NavHostController 目标;
  2. 设置回来键的Dispatcher;
  3. 经过 NavigatorProvider 创立 Navigator
  4. 设置从 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 对应的主页,后面还会继续讲到。

之所以在这儿实例化注册的原因,是因为一旦确认了 ActivityNavigatorNavGraphNavigatorNavigation 导航器就无法发动 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 宿主把 DialogFragmentNavigatorFragmentNavigator 注册了,而这两个 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;
    }
}

这儿首要做了三件事:

  1. parser 依据称号获取 Navigator,创立 Destination;
  2. dest.onInflate() 解析参数,解析不同类型节点的特点;
  3. 假如第一个创立的节点是 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,父类完结后就交由对应的子类去解析对应的特点,

浅谈 Android 架构导航 - Navigation的架构与设计

这儿以 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 就能够了。

NavInflaterinflate() 办法中,因为第一个节点是 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);
        }
    }
}

以上都是导航节点解析和创立的流程。如下图:

浅谈 Android 架构导航 - Navigation的架构与设计

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();
    }
}

这儿首要做了三件事:

  1. 经过 node.getNavigatorName() 找到创立这个节点的 Navigator,它实践便是 NavGraph 目标;
  2. 调用 navigate() 建议真实的导航;
  3. 当这个导航履行成功之后就会把本次的节点增加到回退栈傍边 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 就有或许是 AvtivityNavgatiorFragmentNavgatior 以及 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;
}

ActivityNavigatornavigate() 将需求传递的参数放入 intent,增加 Data 数据;对恳求形式进行判别,设置 flag;设置翻开和退出动画作用;最终经过 startActivity() 来完结 Activity 类型的节点的导航才能。

FragmentNavgator

FragmentNavgatornavigate() 的完结:

//#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

下面看下 DialogFragmentNavigatornavigate()

//#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 三种默许类型的导航才能是怎样被完结的,相信你现已找到了答案。流程图如下:

浅谈 Android 架构导航 - Navigation的架构与设计

  1. 首要需求一个承载页面的容器 NavHost,这个容器有个默许的完结 NavHostFragment,app:navGraph 加载导航图 xml;

  2. NavHostFragment 有个 NavController 目标,页面导航都是经过调用它的 navigate 办法完结跳转的;

  3. NavController 经过调用 setGraph() 办法,传入导航资源文件,经过 NavInflater 解析导航资源文件,获取导航资源文件中的节点以及特点,得到 NavGraph;

  4. NavController 内部经过 NavigatorProvider 办理这几种 navigator;

  5. NavController 内经过 mBackStack 办理回退栈,设置回来键的 Dispatcher 监听,popBackStack() 就能够做回退栈的相关操作;

  6. NavHostFragment 在 oncreate 办法中,NavController 增加了四个 navigator,别离是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,别离完结各自的 navigate 办法,进行页面切换。

  7. 在 navigate 办法中,经过设置参数,action,动画等数据后,依据原生方式完结跳转指定页面,一同会把本次的节点增加到回退栈傍边。

优点:

  1. 给 Activity,Fragment,Dialog 供给导航才能的组件。
  2. 导航时可带着参数,指定转场动画。
  3. 支撑deepline页面直达,fragment回退栈办理才能。

缺点:

  1. 非常依靠XML文件,一切的节点都必需求在 mobile_navigation.xml文件中来界说,这是不行灵活,不利于模块化,组件化开发。
  2. Fragment 类型的节点来履行导航的时分运用的 replace() 办法会导致页面从头加载重走生命周期办法,不行友好。
  3. 不支撑导航进程的阻拦和监听。

这是从零到一建立一个组件化 + 模块化 + 协程 + Flow + Jetpack + MVVM的App,项目地址:github.com/suming77/Su…

点关注,不迷路


好了各位,以上便是这篇文章的悉数内容了,很感谢您阅览这篇文章。我是suming,感谢各位的支撑和认可,您的点赞便是我创作的最大动力。山水有相逢,咱们下篇文章见!

本人水平有限,文章难免会有过错,请批评指正,不胜感激 !

参阅链接:

  • Navigation官网
  • Jetpack 导航

希望咱们能成为朋友,在 Github、 上一同分享知识,一同共勉!Keep Moving!