Android Jetpack - Navigation 使用及原理

官方文档:The Navigation Architecture Component
官方教程:Navigation Codelab

简介

Navigation 用于管理 APP 页面跳转导航,切换 fragment 更加直观,可视化界面展示 fragment 的切换流程图

Demo 地址:https://github.com/wuchao226/AndroidJetpack/tree/master

一、在项目中使用 Navigation

1. 在 Module下的 build.gradle 中添加以下依赖:
def nav_version = "2.3.2"

// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"

// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

如果你要使用SafeArgs插件,还要在 Project 目录下的 build.gradle 文件添加:

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
    }
}

以及 Module 下面的 build.gradle 文件添加:

apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'

涉及到的类关系

  • NavController 控制导航的跳转和弹出栈
  • NavOptions 控制跳转过程中的配置选项, 例如动画和singleTop模式
  • Navigation 工具类 创建点击事件或者获取控制器
  • NavHostFragment 导航的容器, 可以设置和获取导航图(NavGraph)
  • NavGraph 用于描述导航中页面关系的对象 可以增删改查页面,设置起始页等
  • NavigationUI 用于将导航和一系列菜单控件自动绑定的工具类
  • Navigator 页面的根接口, 如果想创建一个新的类型页面就要自定义他
  • NavDeepLinkBuilder 构建一个能打开导航页面的Intent
2. 创建 Navigation 导航
  1. 在资源文件 res 目录下创建 navigation 目录 -> 右击 navigation 目录 New 一个Navigation resource file
  2. 创建一个Destination,在此之前,我已经写好了一个 LoginWelcomeFragment、LoginFragment 和 RegisterFragment

下面看内容组成,login_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"
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcomeFragment">

    <fragment
        android:id="@+id/welcomeFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.LoginWelcomeFragment"
        android:label="WelcomeFragment">
        <action
            android:id="@+id/action_welcomeFragment_to_loginFragment"
            app:destination="@id/loginFragment"/>
        <action
            android:id="@+id/action_welcomeFragment_to_registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:destination="@id/registerFragment"/>
    </fragment>

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.LoginFragment"
        android:label="LoginFragment"/>

    <fragment
        android:id="@+id/registerFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.RegisterFragment"
        android:label="RegisterFragment">
        <argument
            android:name="EMAIL"
            android:defaultValue="wu@163.com"
            app:argType="string"/>
    </fragment>
</navigation>

下面是 navigation 标签的属性:

属性 解释
app:startDestination 默认的起始位置

下面是 fragment 标签的属性:

属性 解释
name 表示所属的 fragment 类
action destination 属性用于指定下一个目标 fragment、进入退出动画等
argument 用于传递数据
3. 建立 NavHostFragment

创建一个新的 LoginActivity,在activity_login.xml文件中:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.LoginActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/login_navigation"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
属性 解释
android:name 值必须是androidx.navigation.fragment.NavHostFragment,声明这是一个 NavHostFragment,表示这是一个可以切换 fragment 的控件
app:navGraph 指定上面的导航意图文件login_navigation.xml ,也就是确定了 Navigation Graph
app:defaultNavHost="true" 表示是否拦截返回键,默认为 false, 假如页面处于第二个 fragment,这时按下返回键,会退出当前 activity 而不是回到上一个fragment。true 则反之
4. 界面跳转、参数传递和动画

在 LoginWelcomeFragment 中,点击登录和注册按钮可以分别跳转到 LoginFragment 和 RegisterFragment 中。

使用了两种方式实现:

一 利用ID导航

目标:LoginWelcomeFragment 携带 key 为 name 的数据跳转到 LoginFragment,LoginFragment 接收后显示。

btn_login.setOnClickListener {
    //设置动画参数
    val navOption = navOptions {
        anim {
            enter = R.anim.slide_in_right
            exit = R.anim.slide_out_left
            popEnter = R.anim.slide_in_left
            popExit = R.anim.slide_out_right
        }
      }
      // 参数设置
      val bundle = Bundle()
      bundle.putString("name", "wu")
      findNavController().navigate(R.id.loginFragment, bundle, navOption)

LoginFragment 的接收代码如下:

val name = arguments?.getString("name")
if (!TextUtils.isEmpty(name)) {
    et_account.setText(name)
}
二 利用Safe Args

LoginWelcomeFragment 通过 Safe Args 将数据传到 RegisterFragment,RegisterFragment 接收后显示。
再看一下已经展示过的 login_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"
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcomeFragment">

    <fragment
        android:id="@+id/welcomeFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.LoginWelcomeFragment"
        android:label="WelcomeFragment">
        <action
            android:id="@+id/action_welcomeFragment_to_loginFragment"
            app:destination="@id/loginFragment"/>
        <action
            android:id="@+id/action_welcomeFragment_to_registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:destination="@id/registerFragment"/>
    </fragment>

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.LoginFragment"
        android:label="LoginFragment"/>

    <fragment
        android:id="@+id/registerFragment"
        android:name="com.wuc.jetpack.ui.fragment.login.RegisterFragment"
        android:label="RegisterFragment">
        <argument
            android:name="EMAIL"
            android:defaultValue="wu@163.com"
            app:argType="string"/>
    </fragment>
</navigation>

action 标签

属性 作用
app:destination"@+id/loginFragment" 跳转完成到达的fragment的Id
app:popUpTo="@id/home_dest" 将 fragment 从栈中弹出,直到某个 Id 的 fragment
app:popUpToInclusive="true/false" 弹出栈是否包含目标
app:launchSingleTop="true/false" 是否开启singleTop模式

argument 标签

属性 作用
android:name 标签名字
app:argType 标签的类型
android:defaultValue 默认值

LoginWelcomeFragment 中的 "注册" 按钮点击事件:

btn_register.setOnClickListener {
    // 利用SafeArgs传递参数
    val action = LoginWelcomeFragmentDirections
       .actionWelcomeFragmentToRegisterFragment()
       .setEMAIL("wu5@Gamil.com")
    findNavController().navigate(action)
}

RegisterFragment中的接收:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val safeArgs:RegisterFragmentArgs by navArgs()
    val email = safeArgs.email
    et_email.setText(email)
}

下面是上述介绍的效果图:

preview_login.gif

二、Navigation 绑定 BottomNavigationView

先在 navigation 目录下新创建了 main_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/main_navigation"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.wuc.jetpack.ui.fragment.main.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home"/>

    <fragment
        android:id="@+id/scoreFragment"
        android:name="com.wuc.jetpack.ui.fragment.main.ScoreFragment"
        android:label="fragment_market"
        tools:layout="@layout/fragment_score"/>

    <fragment
        android:id="@+id/meFragment"
        android:name="com.wuc.jetpack.ui.fragment.main.MeFragment"
        android:label="fragment_me"
        tools:layout="@layout/fragment_me"/>
</navigation>

下面则是 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:orientation="vertical"
    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="0dp"
        android:layout_weight="1"
        app:navGraph="@navigation/main_navigation"
        app:defaultNavHost="true"/>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigation_view"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@android:color/white"
        app:menu="@menu/menu_main"
        app:labelVisibilityMode="labeled"
        app:itemHorizontalTranslationEnabled="false"
        app:itemIconTint="@color/select_item_color"
        app:itemTextColor="@color/select_item_color"/>
</androidx.appcompat.widget.LinearLayoutCompat>

下面是 menu_main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
      xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/homeFragment"
        android:icon="@drawable/btn_nav_home_press"
        android:title="主页"
        tools:ignore="HardcodedText"/>

    <item
        android:id="@+id/scoreFragment"
        android:icon="@drawable/btn_nav_score_normal"
        android:title="积分"
        tools:ignore="HardcodedText"/>

    <item
        android:id="@+id/meFragment"
        android:icon="@drawable/btn_nav_user_normal"
        android:title="我"
        tools:ignore="HardcodedText"/>
</menu>

注意:navigation 里 fragment 的 id 需要和 menu 里 item 的 id 对应

MainActivity 中的处理如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 获取NavController
        val navController = findNavController(R.id.nav_host_fragment)
        // 把BottomNavigationView与NavController绑定。
        // 绑定后,当菜单项被选中时,会调用NavigationUI.onNavDestinationSelected(menuItem, navController)方法
        // 这里有个注意事项,上面创建菜单时有说明,下面手动绑定的注释也有说明
        // BottomNavigationView拥有setupWithNavController()方法,是因为Navigation组件依赖里对BottomNavigationView进行了扩展:
        // fun BottomNavigationView.setupWithNavController(navController: NavController) {
        //    NavigationUI.setupWithNavController(this, navController)
        // }
        navigation_view.setupWithNavController(navController)

        // 如果不用上面的方式,可以用下面的方式自己手动把菜单选中事件与Navigation的导航事件绑定
        /*navigation_view.setOnNavigationItemSelectedListener { menuItem ->
            // 导航到与menuItem菜单项关联的NavDestination,即与menu.xml中item的id相同的destinationId
            // destinationId即navigation/nav_graph.xml中fragment的id
            NavigationUI.onNavDestinationSelected(menuItem, navController)
        }*/
    }
}

下面是效果图:

preview_main.gif

参考文章:

谷歌实验室
Android官方架构组件Navigation:大巧不工的Fragment管理框架
即学即用Android Jetpack - Navigation

三、核心原理

NavHostFragment 是在 Activity 里创建的,下面先分析 NavHostFragment 源码。

3.1 初始化过程 NavHostFragment 生命周期方法

NavHostFragment 的创建,NavHostFragment 的 create 方法。

NavHostFragment#create
   @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId) {
        return create(graphResId, null);
    }


    @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId,
            @Nullable Bundle startDestinationArgs) {
        Bundle b = null;
        if (graphResId != 0) {
            b = new Bundle();
            b.putInt(KEY_GRAPH_ID, graphResId);
        }
        if (startDestinationArgs != null) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
        }

        final NavHostFragment result = new NavHostFragment();
        if (b != null) {
            result.setArguments(b);
        }
        return result;
    }
  1. 初始化 Bundle,并且将 graphResId,startDestinationArgs 存储在 Bundle 中,即将 app:navGraph="@navigation/login_navigation" 中 login_navigation 文件的id,graphResId 放进 Bundle
  2. 新建一个 NavHostFragment,把 bundle 里的数据设置给 NavHostFragment,最后返回一个新的 NavHostFragment;相当于把 activity_login.xml 里的 NavHostFragment 和 login_navigation.xml 文件绑定了。

通过打断点,我们发现 NavHostFragment 里生命周期各个方法的执行顺序是 onInflate、onCreate、onCreateNavController、onCreateView、onViewCreated

XML 文件的解析 NavHostFragment#onInflate
    @CallSuper
    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
            @Nullable Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);

        final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
        final int graphId = navHost.getResourceId(
                androidx.navigation.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();
    }
  1. 主要是解析 activity_login.xml 布局文件里的里的 fragment 标签包裹的一些 xml属性值,主要是两个属性:defaultNavHost 和 navGraph,并且初始化全局变量 。

  2. 获取 xml 里的导航图的 graphId,并将 graphId 赋值给 NavHostFragment 的成员变量 mGraphId,最后设置 defaultNavHost的值(defaultNavHost 值为 true 就可以实现拦截系统 back 键)。

  3. NavHostFragment.onInflate 方法 当 Fragment 以 XML 的方式静态加载时,最先会调用 onInflate 的方法(调用时机:Fragment 所关联的 Activity 在执行 setContentView 时)

NavHostFragment#onCreate 导航初始化
    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        final Context context = requireContext();
        // 始化 NavController,NavController 为导航的控制类,核心类。
        mNavController = new NavHostController(context);
        mNavController.setLifecycleOwner(this);
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        // Set the default state - this will be updated whenever
        // onPrimaryNavigationFragmentChanged() is called
        mNavController.enableOnBackPressed(
                mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
        mIsPrimaryBeforeOnCreate = null;
        mNavController.setViewModelStore(getViewModelStore());
        onCreateNavController(mNavController);

        Bundle navState = null;
        if (savedInstanceState != null) {// 开始恢复状态
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                mDefaultNavHost = true;
                getParentFragmentManager().beginTransaction()
                        .setPrimaryNavigationFragment(this)
                        .commit();
            }
            mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
        }

        if (navState != null) {
            // Navigation controller state overrides arguments
            mNavController.restoreState(navState);
        }
        if (mGraphId != 0) {// 设置导航图信息
            // Set from onInflate()
            mNavController.setGraph(mGraphId);
        } else {
            // See if it was set by NavHostFragment.create()
            final Bundle args = getArguments();
            final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
            final Bundle startDestinationArgs = args != null
                    ? args.getBundle(KEY_START_DESTINATION_ARGS)
                    : null;
            if (graphId != 0) {
                mNavController.setGraph(graphId, startDestinationArgs);
            }
        }

        // We purposefully run this last as this will trigger the onCreate() of
        // child fragments, which may be relying on having the NavController already
        // created and having its state restored by that point.
        super.onCreate(savedInstanceState);
    }
    @CallSuper
    public void setGraph(@NavigationRes int graphResId) {
        setGraph(graphResId, null);
    }

    @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }

    @CallSuper
    public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
        if (mGraph != null) {
            // Pop everything from the old graph off the back stack
            popBackStackInternal(mGraph.getId(), true);
        }
        mGraph = graph;
        onGraphCreated(startDestinationArgs);
    }
    @SuppressLint("ResourceType")
    @NonNull
    public NavGraph inflate(@NavigationRes int graphResId) {
        Resources res = mContext.getResources();
        XmlResourceParser parser = res.getXml(graphResId);
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }

            String rootElement = parser.getName();
            NavDestination destination = inflate(res, parser, attrs, graphResId);
            if (!(destination instanceof NavGraph)) {
                throw new IllegalArgumentException("Root element <" + rootElement + ">"
                        + " did not inflate into a NavGraph");
            }
            return (NavGraph) destination;
        } catch (Exception e) {
            throw new RuntimeException("Exception inflating "
                    + res.getResourceName(graphResId) + " line "
                    + parser.getLineNumber(), e);
        } finally {
            parser.close();
        }
    }
  1. new NavHostController(context); 创建一个控制器 mNavController,把 mNavController 和 Lifecycle 建立绑定关系(监听生命周期有关);把 mNavController 和 ViewModelStore 建立关系(数据保存有关)。

  2. onCreateNavController(mNavController); 将 mNavController 传入,根据新建这个控制器对应的 navigator,并把 navigator 和它对应的名字放进数组 mNavigators 里。

  3. 通过 mNavController.setGraph(mGraphId) ,根据导航图的 mGraphId 将导航图 NavGraph 和控制器 mNavController 关联起来,NavGraph 里又会通过 inflate方法解析导航图 xml 文件,并最后通过 addDestination 将目的地信息添加到到NavDestination,(控制器 mNavController 间接持有 NavDestination 数组: Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>();)

NavBackStackEntry 类其实是包装了 NavDestination 类的;
NavInflater 主要就是解析导航图 xml 信息的。

  1. NavInflater.inflate 方法根据传入的 XML 资源 id 构建 NavGraph,NavGraph 组成 Fragment 路由的导航地图,而 NavDestination 代表了导航的每一个目的地。在解析完 NavDestination 后,需要要求 NavDestination 为 NavGraph,即 NavGraph 是 NavDestination 的子类。而且在 NavGraph 内部存储了
    NavDestination 信息。

  2. 上面的 inflate方法内部会继续调用 inflate 方法。
    (1)getNavigator方法获取都 Navigator 实例,该实例在构建 NavController 时被添加进去,这里获取的是 FragmentNavigator 对象。
    (2)createDestination方法,会调用 FragmentNavigator 的 createDestination 构建 Destination 对象。
    (3)onInflate 方法,解析 destination XML
    (4)while 循环内部通过递归构建导航图。

  3. 通过 NavInflater 类之后,解析了 XML 文件构建整个 Graph 之后,下面回到setGraph 方法,在解析完 XML 后会,回到 NavHostFragment.setGraph 方法。
    (1)popBackStackInternal 方法将回退栈中的信息全部出栈。
    (2)调用 onGraphCreated 主要是显示一个导航 Fragment 视图。

  4. onGraphCreated 方法
    (1)恢复之前的导航状态
    (2)调用 navigate 方法,显示第一个 Fragment。即在 Navigation 文件里,属性app:startDestination 的 Fragment。所以最终都会走到 navigate 导航方法。

NavHostFragment#onCreateNavController

    onCreateNavController(mNavController);

    @SuppressWarnings({"WeakerAccess", "deprecation"})
    @CallSuper
    protected void onCreateNavController(@NonNull NavController navController) {
        navController.getNavigatorProvider().addNavigator(
                new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
        navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }

    @Nullable
    public final Navigator<? extends NavDestination> addNavigator(
            @NonNull Navigator<? extends NavDestination> navigator) {
        String name = getNameForNavigator(navigator.getClass());

        return addNavigator(name, navigator);
    }

    @CallSuper
    @Nullable
    public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
            @NonNull Navigator<? extends NavDestination> navigator) {
        if (!validateName(name)) {
            throw new IllegalArgumentException("navigator name cannot be an empty string");
        }
        return mNavigators.put(name, navigator);
    }
  1. 在实现导航的时候,我们需要根据 navigation 配置文件生成 NavGraph 类,然后在根据每个不同的 action id,找到对应的 NavDestination 就可以实现页面导航跳转了。

  2. 其中 mNavigatorProvider 是 NavController 中的全局变量,内部通过 HashMap 键值对的形式保存 Navigator 类。

  3. createFragmentNavigator 方法,构建了 FragmentNavigator 对象,其中抽象类 Navigator 还有个重要的实现类 ActivityNavigator 和 NavGraphNavigator。
    这个两个类的对象在 NavController 的构造方法中被添加。

    public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }

其中 Navigator 类的作用是:能够实例化对应的 NavDestination,并且能够实现导航功能,拥有自己的回退栈。

NavHostFragment#onCreateView
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        // When added via XML, this has no effect (since this FragmentContainerView is given the ID
        // automatically), but this ensures that the View exists as part of this Fragment's View
        // hierarchy in cases where the NavHostFragment is added programmatically as is required
        // for child fragment transactions
        containerView.setId(getContainerId());// 用于以代码方式添加 fragment
        return containerView;
    }

创建顶层容器 FragmentContainerView,并且设置 FragmentContainerView 的 id(FragmentContainerView 是继承 FrameLayout 的)

NavHostFragment#onViewCreated
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!(view instanceof ViewGroup)) {
            throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
        }
        Navigation.setViewNavController(view, mNavController);
        // When added programmatically, we need to set the NavController on the parent - i.e.,
        // the View that has the ID matching this NavHostFragment.
        if (view.getParent() != null) {
            mViewParent = (View) view.getParent();
            if (mViewParent.getId() == getId()) {
                // 把 mNavController 记录在 view 的 tag 中
                Navigation.setViewNavController(mViewParent, mNavController);
            }
        }
    }

将 mNavController 和 view 绑定起来。后面那段 if 判断的意思是:
(1)当通过 XML 添加时,父 View 是null,我们的 view 就是 NavHostFragment的根 FrameLayout。
(2)但是当以代码方式添加时,需要在父级上设置绑定 NavController(我们也可以在 MainActvity 里直接创建 NavHostFragment,并不一定在布局里创建)。

3.2 导航

在构建和获取到 NavController 对象以及 NavGraph 之后,下面是使用它来实现真正的导航了。下面从 navigate 开始分析。在 navigate 方法内部会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。

    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        // 如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
        NavDestination currentNode = mBackStack.isEmpty()
                ? mGraph
                : mBackStack.getLast().getDestination();
        if (currentNode == null) {
            throw new IllegalStateException("no current navigation node");
        }
        @IdRes int destId = resId;
        // 根据id,获取对应的NavAction。然后在通过NavAction获取目的地id。
        final NavAction navAction = currentNode.getAction(resId);
        Bundle combinedArgs = null;
        if (navAction != null) {
            if (navOptions == null) {
                navOptions = navAction.getNavOptions();
            }
            destId = navAction.getDestinationId();
            Bundle navActionArgs = navAction.getDefaultArguments();
            if (navActionArgs != null) {
                combinedArgs = new Bundle();
                combinedArgs.putAll(navActionArgs);
            }
        }

        if (args != null) {
            if (combinedArgs == null) {
                combinedArgs = new Bundle();
            }
            combinedArgs.putAll(args);
        }

        if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
            popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
            return;
        }

        if (destId == 0) {
            throw new IllegalArgumentException("Destination id == 0 can only be used"
                    + " in conjunction with a valid navOptions.popUpTo");
        }
        // 利用目的地ID属性,通过findDestination方法,找到准备导航的目的地。 
        NavDestination node = findDestination(destId);
        if (node == null) {
            final String dest = NavDestination.getDisplayName(mContext, destId);
            if (navAction != null) {
                throw new IllegalArgumentException("Navigation destination " + dest
                        + " referenced from action "
                        + NavDestination.getDisplayName(mContext, resId)
                        + " cannot be found from the current destination " + currentNode);
            } else {
                throw new IllegalArgumentException("Navigation action/destination " + dest
                        + " cannot be found from the current destination " + currentNode);
            }
        }
        // 开始导航
        navigate(node, combinedArgs, navOptions, navigatorExtras);
    }

一开始会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。
navigate 方法:
(1)如果回退栈为 null 返回 NavGraph,不为 null 返回回退栈中的最后一项。
(2)根据 id,获取对应的 NavAction。然后在通过 NavAction 获取目的地id。
(4)利用目的地 ID 属性,通过 findDestination 方法,找到准备导航的目的地。
(5)根据导航目的地的名字,调用 getNavigator 方法,获取 Navigator 对象。这里对应的是 FragmentNavigator。

NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras);跳下一步

    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            if (!(newDest instanceof FloatingWindow)) {
                // We've successfully navigating to the new destination, which means
                // we should pop any FloatingWindow destination off the back stack
                // before updating the back stack with our new destination
                //noinspection StatementWithEmptyBody
                while (!mBackStack.isEmpty()
                        && mBackStack.peekLast().getDestination() instanceof FloatingWindow
                        && popBackStackInternal(
                                mBackStack.peekLast().getDestination().getId(), true)) {
                    // Keep popping
                }
            }

            // When you navigate() to a NavGraph, we need to ensure that a new instance
            // is always created vs reusing an existing copy of that destination
            ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
            NavDestination destination = newDest;
            if (node instanceof NavGraph) {
                do {
                    NavGraph parent = destination.getParent();
                    if (parent != null) {
                        NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
                                finalArgs, mLifecycleOwner, mViewModel);
                        hierarchy.addFirst(entry);
                        // Pop any orphaned copy of that navigation graph off the back stack
                        if (!mBackStack.isEmpty()
                                && mBackStack.getLast().getDestination() == parent) {
                            popBackStackInternal(parent.getId(), true);
                        }
                    }
                    destination = parent;
                } while (destination != null && destination != node);
            }

            // Now collect the set of all intermediate NavGraphs that need to be put onto
            // the back stack
            destination = hierarchy.isEmpty()
                    ? newDest
                    : hierarchy.getFirst().getDestination();
            while (destination != null && findDestination(destination.getId()) == null) {
                NavGraph parent = destination.getParent();
                if (parent != null) {
                    NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
                            mLifecycleOwner, mViewModel);
                    hierarchy.addFirst(entry);
                }
                destination = parent;
            }
            NavDestination overlappingDestination = hierarchy.isEmpty()
                    ? newDest
                    : hierarchy.getLast().getDestination();
            // Pop any orphaned navigation graphs that don't connect to the new destinations
            //noinspection StatementWithEmptyBody
            while (!mBackStack.isEmpty()
                    && mBackStack.getLast().getDestination() instanceof NavGraph
                    && ((NavGraph) mBackStack.getLast().getDestination()).findNode(
                            overlappingDestination.getId(), false) == null
                    && popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
                // Keep popping
            }
            mBackStack.addAll(hierarchy);
            // The mGraph should always be on the back stack after you navigate()
            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.addFirst(entry);
            }
            // And finally, add the new destination with its default args
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            launchSingleTop = true;
            NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
            if (singleTopBackStackEntry != null) {
                singleTopBackStackEntry.replaceArguments(finalArgs);
            }
        }
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null || launchSingleTop) {
            dispatchOnDestinationChanged();
        }
    }

从 mNavigatorProvider 拿出对应的 navigator,然后调用 Navigator 的 navigate,将目的地,动画参数,跳转参数传入实现跳转
,而真正实现这个抽象方法的是在 FragmentNavigator 和 ActivityNavigator 的跳转方法里。我们看到 FragmentNavigator 里(ActivityNavigator里的实现更简单)

抽象类Navigator具体实现.png

FragmentNavigator#navigate 方法

    @SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        // 通过反射机制构建Fragment实例  
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            // 处理进出场等动画逻辑  
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }

        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.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
                // 通过mFragmentManager把fragment出栈
                mFragmentManager.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

(1)调用 instantiateFragment,通过反射机制构建 Fragment 实例
(2)处理进出场等动画逻辑 , 出入场动画都在 NavOptions 类里
(3)最终调用FragmentManager来处理导航逻辑;通过 mFragmentManager 把 fragment 出栈,入栈最后通过事务的提交 fragment。
ActivityNavigator最终也是调用了startActivity方法

总结

Navigation 的核心源码每个类的作用:

  • NavHosFragment
    就是activty要绑定的Fragment,它和我们的导航xml绑定在一起,可以理解为实现导航的主要的Fragment

  • NavHostController
    导航控制器,也是整个Navigation源码里的核心类,是在NavHosFragment的onCreate方法里初始化和做一些关联操作的。用于中转控制xml解析,navigate导航等一系列主要操作。

  • NavInflater
    主要用于导航xml图的解析工作

  • NavGraph
    保存NavInflater解析后的目的地信息

  • NavDestination
    目的地实体类

  • NavigatorProvider
    导航Navigator类的提供者

  • Navigator
    导航类,提供导航

  • NavAction
    导航动作信息类,保存导航入场动画等信息

    Navigation核心类图.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270