Jetpack Navigation学习笔记(源码分析)

Jetpack Navigation学习笔记

官方文档:https://developer.android.google.cn/guide/navigation?hl=zh-cn

官方API: https://developer.android.google.cn/jetpack/androidx/releases/navigation

简介

官方对Navigation的描述:Navigation 是一个框架,用于在 Android 应用中的“目标位置”之间导航。该框架提供一致的 API,无论目标位置是作为 Fragment、Activity 还是其他组件实现的。

通过官方的描述我们大概可以得出Navigation是一个用同一API进行目标(Destination)页面之间进行跳转的框架。

通过官方文档我们可以知道Navigation从三个部分组成:

  • 导航图(NavGraph)

    在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。

  • 导航宿主(NavHost)

    显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现 NavHostFragment,用于显示导航中指定的 Fragment 目标。

  • 导航控制器(NavController)

    NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。

导航组件提供各种其他优势,包括以下内容:

  • 处理 Fragment 事务。
  • 默认情况下,正确处理往返操作。
  • 为动画和转换提供标准化资源。
  • 实现和处理深层链接。
  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
  • Safe Args 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。
  • ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。

Navigation的使用(Kotlin为例)

主要是为了阅读Navigation的源码,所以在这里就做很简单很简洁的用法,深入的用法不追随。

添加依赖

在要使用Navigation的module的build.gradle文件dependencies闭包添加以下依赖

implementation "androidx.navigation:navigation-fragment-ktx:2.3.4"
implementation "androidx.navigation:navigation-ui-ktx:2.3.4"

创建导航图(NavGraph)

导航是发生在应用中的各个目的地之间,这些目的地都是通过操作Action连接,导航图是一种资源文件,该资源文件包含这个应用所使用的目的地与对这些目的地的操作,显示应用中所有的导航路径。

导航图文件位于res资源目录中的navigation目录,我们在此目录创建我们的导航图资源文件nav_graph.xml

image-20210417190839734

该xml文件的根标签为navigation标签,结构为如下:

<?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/nav_graph">

</navigation>

我们的导航图所包含的对导航目标的描述都编写在此文件下面,我可以通过AndroidStudio的师徒编辑器对此文件进行编辑也可以通过手动的方式对该文件进行编写。

比如我们要在此导航图添加一个目标,那么我们可以通过右侧的视图编辑器添加我们的目标进行添加:

image-20210417191702624

进行上述操作后我们的导航图文件navigation标签中会加入我们的目标相关描述标签:

image-20210417191837689

通过上面我们可以看出我们的目标是一个Fragment,目标的ID为fragmentA、该Fragment为我们位于com.skit.navigation.fragment包下的FramentA

我们在简介说过Navigation支持Fragment、Activity或其他组件,那么我们的Activity如何添加?

其实我们看上面的目标标签大概可以推测出添加Activity所需要编写目标标签:

...
    <activity
        android:id="@+id/activityA"
        android:name="com.skit.navigation.activity.ActivityA"
        android:label="ActivityA" />
...

PS: 在我们对导航图文件进行编辑之后我们通过视图编辑器可以看到视图编辑器所展现的是我们所添加的目标,但是我们在看视图编辑器的时候可能第一时间可能无法很快定位目前目标是那个目标,我们可以通过给目标添加tools:layout属性并指定目标布局,经过此操作后视图编辑器可以给我们展示出目标的布局方便我们进行快速辨认。

image-20210417193041416image-20210417193721142

向Activity添加NavHost并运行

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

    <fragment
        android:id="@+id/nav_host_fragment_container"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

注意事项:

  • android:name属性一定得指定实现NavHost接口的类名,navigation默认自带一个NavHostFragment,此Fragment实现了NavHost接口
  • app:defaultNavHost表示我们所指定的NavHostFragment可以拦截系统的返回事件。
  • app:navGraph指定我们的导航图文件,此属性将会让导航图与NavFragment进行关联,导航图会在NavFragment进行对导航目标的操作。

接下来我们编译运行Demo,可以发现我们所设置的起始目标显示在我们的NavHost中。

image-20210417195444713

导航到目标

接下来我们把FragmentB当作目标添加到到导航图中,并给Fragment A中的TextView添加一个点击事件导航到FragmentB这个目标中,我们在视图编辑器通过拖动fragmentA目标右侧的小圆点到fragmentB连接我们的目的地。连接的目的地在视图编辑器中会用箭头指向标识。

image-20210417200127125image-20210417200335457

<?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/nav_graph"
    app:startDestination="@id/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.skit.navigation.fragment.FragmentA"
        android:label="FragmentA"
        tools:layout="@layout/fragment_a">
        <action
            android:id="@+id/action_fragmentA_to_fragmentB4"
            app:destination="@id/fragmentB" />
    </fragment>

    ...

    <fragment
        android:id="@+id/fragmentB"
        android:name="com.skit.navigation.fragment.FragmentB"
        android:label="FragmentB"
        tools:layout="@layout/fragment_b" />
</navigation>

两个目标的连接会生成action标签,action包含有该action的id与该action的目标id,当然我们可以在action进行动画的指定等操作,我们使用NavController提供的navigate放并指定action的id进行导航操作。

获取NavController对象的方法:

  • NavController.findNavController(view)

  • view.findNavController()

  • 在Activity里面NavController.findNavController(activity, viewId)

FragmentA:

class FragmentA : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_a, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        view.findViewById<TextView>(R.id.tv_fragment_a).setOnClickListener {
//            Navigation.findNavController(view).navigate(R.id.action_fragmentA_to_fragmentB4)
            it.findNavController().navigate(R.id.action_fragmentA_to_fragmentB4)
        }
	      ...
    }
}

导航到深度链接(DeepLink)

在导航图资源文件中需要使用deeplink打开的目标标签里面添加<deeplink>标签

<navigation
            .....
           >
   <fragment
        android:id="@+id/fragmentB"
        android:name="com.skit.navigation.fragment.FragmentB"
        android:label="FragmentB"
        tools:layout="@layout/fragment_b">
        <deepLink
            android:id="@+id/deepLink"
            app:uri="app://www.xxx.com/{name}" />
    </fragment>
</navigation>

并且在清单文件中Activity添加<nav-graph>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.skit.navigation">

    <application
       .....>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <nav-graph android:value="@navigation/nav_graph" />
        </activity>
      .....
    </application>

</manifest>

在Fragment中就可以通过arguments获取deeplink传过来的内容

可以使用 adb shell am start -a android.intent.action.VIEW -d “app://www.xxx.com/Musk” 来模拟对这个链接的请求ACTION_VIEW

image-20210418001555912

对Navigation大致的用法结束了,接下来进入正题,通过源码来了解Navigation这个框架与它的结构。

Navigation源码分析

在看Navigation源码前提出一个问题,在我第一次尝试将BottomNavigationView与Navigation结合使用的时候发现Fragment每次都会重新走一次onCreateView,为什么会出现这个问题?带着问题去看源码。

通过先前的Demo代码能发现NavHostFragment作为容器,所有的导航操作都是经过NavHostFragment来进行,在NavHostFragment中又会委托给NavController,也是就是说阅读Navigation源码我们可以从NavHostFragment开始进行。

NavHostFragment

NavHostFragment

image-20210418103124403

通过上图我们可以知道,NavHostFragment继承与Fragment,并且重写了Fragment生命周期时的几个方法,还可以发现这个类本身提供了三个静态方法:

create(int)

create(int,Bundle)

findNavController(Fragment)

通过官方文档与代码注释我们可以知道NavHostFragment的初始化主要有两种实现方式:

  • 配置XML文件

  • 代码实现

    val navHostFragment = NavHostFragment.create(R.navigation.nav_graph)
    supportFragmentManager.beginTransaction()
        .replace(R.id.nav_host_fragment_container, navHostFragment)
        .setPrimaryNavigationFragment(navHostFragment)
        .commit()
    

在这里提供的两个create方法作用就是在代码实现时提供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);//设置arguments
  }
  return result;
}

NavHostFragment.create方法:

  • 初始化Bundle,并且将graphResId、startDestinationArgs存储在Bundle中。
  • 返回NavHostFragment实例。
onInflate方法

onInflate方法是Fragment的生命周期方法,当Fragment以XML的方式静态加载时,最先会调用onInflate的方法,该方法调用时机在Fragment所关联的Activity在执行setContentView时。

该方法主要用于获取xml中定义的属性与值(在我们在xml中使用<fragment>或者FragmentContainerView时就会间接调用次方法)。

@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);//获取NavHost styleable
    final int graphId = navHost.getResourceId(
            androidx.navigation.R.styleable.NavHost_navGraph, 0);//获取navGraph属性值
    if (graphId != 0) {
        mGraphId = graphId;
    }
    navHost.recycle();//回收资源

    final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);//获取NavHostFragment styleable
    final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);//获取defaultNavHost属性值
    if (defaultHost) {
        mDefaultNavHost = true;
    }
    a.recycle();//回收资源
}

NavHostFragment.onInflate方法:

  • 从xml解析获取navGraph、defaultNavHost这两个属性
  • defaltNavHost为true时,NavHostFragment将会通过FragmentManager 切换到回退栈顶部,并且可以拦截返回键事件
onCreate

onCreate也是Fragment的生命周期方法,该方法不管该Fragment是xml或代码实现都会被调用。

@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    final Context context = requireContext();
  	//创建NavHostController
    mNavController = new NavHostController(context);
  	//设置lifecycleOwner为当前的Fragment
    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
protected void onCreateNavController(@NonNull NavController navController) {
  navController.getNavigatorProvider().addNavigator(
    new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
  navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}

@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
  return new FragmentNavigator(requireContext(), getChildFragmentManager(),
                               getContainerId());
}

通过上面的代码可以知道:

  • 创建初始化NavHostController
  • NavigatorProvider中以键值对保存DialogFragmentNavigator、FragmentNavigator
  • savedInstanceState不为空时恢复NavHostController状态
  • 将graph设置给NavHostController
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());
    return containerView;
}

该Fragment只有一个布局FragmentContainerView该View继承与FrameLayout

我们现在在xml布局使用<fragment>标签可以发现官方会推荐我们使用FragmentContainerView替换该标签

image-20210418112852458
/**
*FragmentContainerView is a customized Layout designed specifically for Fragments. It extends
* {@link FrameLayout}, so it can reliably handle Fragment Transactions, and it also has additional
* features to coordinate with fragment behavior.
* ...
*/

看该自定义View的类,通过注释可以知道这是一个专门为Fragment所设计的自定义布局,可以可靠地处理Fragment Transaction,并且它还具有其他特性来协调Fragment的行为

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()) {
            Navigation.setViewNavController(mViewParent, mNavController);
        }
    }
}
  • 当通过XML添加时,父View是null,我们的view就是NavHostFragment的根。
  • 但是当以代码方式添加时PS: 即调用NavFragment.create,需要在父级上设置NavController。
Navigation.setViewNavController
public static void setViewNavController(@NonNull View view,
        @Nullable NavController controller) {
    view.setTag(R.id.nav_controller_view_tag, controller);
}
  • 主要是将NavController对象设置为rootView的tag,方便以后递归遍历到NavController对象,确保NavController对象的唯一性。

NavController的获取

之前我们使用导航功能的时候说过如果要进行导航就需要获取NacController,NavController的获取方式我们也列举了几个。

  • 通过View的findNavController方法
  • NavHostFragment的findNavController方法
  • Navigation的findNavController方法

我们来看看每个的实现:

View#findNavController
fun View.findNavController(): NavController =
        Navigation.findNavController(this)

可以看到View的findNavController方法是一个Kotlin扩展方法,实际上还是调用的Navigation.findNavController方法

NavHostFragment#findNavController
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
    Fragment findFragment = fragment;
    while (findFragment != null) {
        if (findFragment instanceof NavHostFragment) {
            return ((NavHostFragment) findFragment).getNavController();
        }
        Fragment primaryNavFragment = findFragment.getParentFragmentManager()
                .getPrimaryNavigationFragment();
        if (primaryNavFragment instanceof NavHostFragment) {
            return ((NavHostFragment) primaryNavFragment).getNavController();
        }
        findFragment = findFragment.getParentFragment();
    }

    // Try looking for one associated with the view instead, if applicable
    View view = fragment.getView();
    if (view != null) {
        return Navigation.findNavController(view);//1
    }

    // For DialogFragments, look at the dialog's decor view
    Dialog dialog = fragment instanceof DialogFragment
            ? ((DialogFragment) fragment).getDialog()
            : null;
    if (dialog != null && dialog.getWindow() != null) {
        return Navigation.findNavController(dialog.getWindow().getDecorView());//2
    }

    throw new IllegalStateException("Fragment " + fragment
            + " does not have a NavController set");
}

通过1、2标识的位置可以知道NavHostFragment的findNavController方法最终还是调用的Navigation.findNavController方法进行对NavController的获取

Navigation#findNavConroller
@NonNull
public static NavController findNavController(@NonNull View view) {
  NavController navController = findViewNavController(view);
  if (navController == null) {
    throw new IllegalStateException("View " + view + " does not have a NavController set");
  }
  return navController;
}

//循环查找controller
@Nullable
private static NavController findViewNavController(@NonNull View view) {
  while (view != null) {
    NavController controller = getViewNavController(view);
    if (controller != null) {
      return controller;
    }
    ViewParent parent = view.getParent();
    view = parent instanceof View ? (View) parent : null;
  }
  return null;
}

//通过View的tag获取controller
@Nullable
private static NavController getViewNavController(@NonNull View view) {
  Object tag = view.getTag(R.id.nav_controller_view_tag);
  NavController controller = null;
  if (tag instanceof WeakReference) {
    controller = ((WeakReference<NavController>) tag).get();
  } else if (tag instanceof NavController) {
    controller = (NavController) tag;
  }
  return controller;
}

Navigation.findNavController方法最终会通过View的tag来获取我们的NavController,而这里的key为nav_controller_view_tag的tag就是在Navigation.setViewNavController设置的tag。

导航的实现

我们在实现导航的时候需要通过导航图生成NavGraph对象,然后根据导航图中不同的目标与action找到对应的NavDestination从而实现导航。

在NavHostFragment的oncreate方法中调用了onCreateNavController(mNavController)

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

@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
  return new FragmentNavigator(requireContext(), getChildFragmentManager(),
                               getContainerId());
}

在这时对当前NavController对象的NavigatorProvider添加了DialogFragmentNavigatorFragmentNavigator

那么,这个NavigatorProvider是什么?通过名字我们可以初步推断出这是一个导航调度器。

getNavigatorProvider方法
private NavigatorProvider mNavigatorProvider = new NavigatorProvider();

@NonNull
public NavigatorProvider getNavigatorProvider() {
    return mNavigatorProvider;
}
NavigatorProvider
@SuppressLint("TypeParameterUnusedInFormals")
public class NavigatorProvider {
  //注解名字HashMap
    private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();

  //验证名字方法
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    private static boolean validateName(String name) {
        return name != null && !name.isEmpty();
    }

  //获取Navigator的名字
    @NonNull
    static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
        String name = sAnnotationNames.get(navigatorClass);//从HashMap获取名字
        if (name == null) {//名字为空,通过注解获取
            Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
            name = annotation != null ? annotation.value() : null;
            if (!validateName(name)) {
                throw new IllegalArgumentException("No @Navigator.Name annotation found for "
                        + navigatorClass.getSimpleName());
            }
            sAnnotationNames.put(navigatorClass, name);//存到HashMap里面
        }
        return name;
    }

	 //Navigator HashMap
    private final HashMap<String, Navigator<? extends NavDestination>> mNavigators =
            new HashMap<>();

  //获取Navigator
    @NonNull
    public final <T extends Navigator<?>> T getNavigator(@NonNull Class<T> navigatorClass) {
        String name = getNameForNavigator(navigatorClass);//通过名字获取
        return getNavigator(name);
    }
  
  //获取Navigator
    @SuppressWarnings("unchecked")
    @CallSuper
    @NonNull
    public <T extends Navigator<?>> T getNavigator(@NonNull String name) {
        if (!validateName(name)) {
            throw new IllegalArgumentException("navigator name cannot be an empty string");
        }
      //从HashMap获取Navigator
        Navigator<? extends NavDestination> navigator = mNavigators.get(name);
        if (navigator == null) {
            throw new IllegalStateException("Could not find Navigator with name \"" + name
                    + "\". You must call NavController.addNavigator() for each navigation type.");
        }
        return (T) navigator;
    }
  //添加Navigator
    @Nullable
    public final Navigator<? extends NavDestination> addNavigator(
            @NonNull Navigator<? extends NavDestination> navigator) {
        String name = getNameForNavigator(navigator.getClass());//先获取Navigator的名字
        return addNavigator(name, navigator);//添加Navigator
    }

  //添加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);//存到HashMap
    }
  //获取存Navigator的HashMap
    Map<String, Navigator<? extends NavDestination>> getNavigators() {
        return mNavigators;
    }
}

通过代码可以知道这是一个存储Navigator的类,里面有两个HashMap一个将Navigator的类作为key存储Navigator的名字、一个用通过名字存储Navigator对象,并提供相应的增加、获取方法,我们可以知道这个类就跟自己的类名一样是一个导航调度器。

Navigator

继续看onCreateNavController方法:

protected void onCreateNavController(@NonNull NavController navController) {
  navController.getNavigatorProvider().addNavigator(
    new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
  navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
  return new FragmentNavigator(requireContext(), getChildFragmentManager(),
                               getContainerId());
}

该方法添加了两个Navigator,一个是DialogFragmentNavigator另一个是FragmentNavigator,我们看FragmentNavigator类

FragmentNavigator
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination>{
 .....
}

FragmentNavigator继承于Navigator,我们之前提到的在getNameForNavigator通过注解获取名字就是获取的@Navigator.Name的value。

Navigator抽象类提供了一些方法:

image-20210418133450693

其中这些方法都需要子类来实现具体的操作:

  • 创建Destination:createDestination
  • 导航:navigate
  • 恢复状态:onRestoreState
  • 保存状态:onSaveState
  • 弹出返回栈:popBackStack

Navigator抽象类还有两个实现类NavGraphNavigatorActivityNavigator,这两个Navigator在NavController进行构造的时候创建并加入到这个NavController的NavigatorProvider中。

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));//NavGraphNavigator
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));//ActivityNavigator,主要用于Activity的目标导航
}

我们在进行导航的时候调用NavController对象的navigate方法

//通过ID导航
public void navigate(@IdRes int resId) {
  navigate(resId, null);//调用navigate,bundle为空
}
//通过ID导航并附带Bundle
public void navigate(@IdRes int resId, @Nullable Bundle args) {
  navigate(resId, args, null);
}
//通过ID导航并附带Bundle与NavOptions
public void navigate(@IdRes int resId, @Nullable Bundle args,
                     @Nullable NavOptions navOptions) {
  navigate(resId, args, navOptions, null);
}
//通过ID导航并附带Bundle、NavOptions、Navigator.Extras
@SuppressWarnings("deprecation")
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
                     @Nullable Navigator.Extras navigatorExtras) {
  NavDestination currentNode = mBackStack.isEmpty()
    ? mGraph
    : mBackStack.getLast().getDestination();
  if (currentNode == null) {
    throw new IllegalStateException("no current navigation node");
  }
  @IdRes int destId = resId;
  final NavAction navAction = currentNode.getAction(resId);//通过action ID获取NavAction
  Bundle combinedArgs = null;
  if (navAction != null) {
    if (navOptions == null) {
      navOptions = navAction.getNavOptions();//从NavAction获取NavOptions
    }
    destId = navAction.getDestinationId();//从action获取目标ID
    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");
  }

  NavDestination node = findDestination(destId);//使用目标ID获取NavDestination
  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);//导航
}

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
                      @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
  boolean popped = false;
  boolean launchSingleTop = false;
  .....
  //从导航调度器获取导航器
  Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
    node.getNavigatorName());
  Bundle finalArgs = node.addInDefaultArgs(args);
  //使用navigator.navigate方法进行导航
  NavDestination newDest = navigator.navigate(node, finalArgs,
                                              navOptions, navigatorExtras);
  if (newDest != null) {
    if (!(newDest instanceof FloatingWindow)) {//如果目标是悬浮窗口(DialogFragment)
      while (!mBackStack.isEmpty()
             && mBackStack.peekLast().getDestination() instanceof FloatingWindow
             && popBackStackInternal(
               mBackStack.peekLast().getDestination().getId(), true)) {
        // Keep popping
      }
    }
	.....
}

NavContoller最终会调用Navigator中的navigate方法,具体实现得看具体目标的导航器类,如Fragment的FragmentNavigator;Activity的ActivityNavigator等,我们先看FragmentNavigator的navigate方法来了解一下Navigation下Fragment的切换到底是怎么样的。

FragmentNavigator#navigate
@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();//获取Fragment事务
  ///动画相关开始
  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);
  }
  ///动画相关结束
  
  //使用替换方法替换Fragment
  ft.replace(mContainerId, frag);//注意使用的replace方法
  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) {
    if (mBackStack.size() > 1) {
      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();//提交事务
  ....
}

//实例化Fragment
@Deprecated
@NonNull
public Fragment instantiateFragment(@NonNull Context context,
                                    @NonNull FragmentManager fragmentManager,
                                    @NonNull String className, @SuppressWarnings("unused") @Nullable Bundle args) {
  //使用Fragment工厂实例化Fragment
  return fragmentManager.getFragmentFactory().instantiate(
    context.getClassLoader(), className);
}

首先会获取目标类的类名,然后通过这个类名使用Fragment工厂方法对Fragment进行实例化,实例化过后对这个Fragment对象arguments,还会通过从action获取的切换动画等内容对该Fragment设置切换动画,最终会使用FragmentTransaction的replace方法与commit方法进行对Fragment的替换与提交操作,通过navigate方法我们可以很清楚的看到Navigation切换Fragment是使用的replace,并且会加入返回栈,也就是说每次切换Fragment都会摧毁视图并重新创建视图才会出现我们之前所说的onCreateView重走一遍的问题。

在这里使用的replace也就引起了被很多开发者所认为坑的问题,网上搜索Navigation能搜出来一堆对Fragment在Navigation上使用时的生命周期疑问与所引发的问题。

也就是说办法避免出现这个问题,也就是说需要我们自己自定义一个Fragment的Navigator,并在navigate中使用hide、show来对Fragment进行操作实现隐藏于显示。

导航图文件解析

我们导航图资源文件在NavHostFragment的onCreate方法使用NavController的setGraph方法进行设定,我们从这个部分开始入手

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

@NonNull
public NavInflater getNavInflater() {
  if (mInflater == null) {
    mInflater = new NavInflater(mContext, mNavigatorProvider);
  }
  return mInflater;
}

@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);
}
NavInflater#inflate
@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
    }
    .....
    String rootElement = parser.getName();
    NavDestination destination = inflate(res, parser, attrs, graphResId);//解析获取NavDestination
    if (!(destination instanceof NavGraph)) {
      throw new IllegalArgumentException("Root element <" + rootElement + ">"
                                         + " did not inflate into a NavGraph");
    }
    return (NavGraph) destination;//强转为NavGraph
  ....
}

private static final String TAG_ARGUMENT = "argument";//argument
private static final String TAG_DEEP_LINK = "deepLink";//deeplink
private static final String TAG_ACTION = "action";//action
private static final String TAG_INCLUDE = "include";//include

@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
                               @NonNull AttributeSet attrs, int graphResId)
  throws XmlPullParserException, IOException {
  Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
  final NavDestination dest = navigator.createDestination();//创建目标,由具体的导航器类实现

  dest.onInflate(mContext, attrs);

  final int innerDepth = parser.getDepth() + 1;
  int type;
  int depth;
  while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
         && ((depth = parser.getDepth()) >= innerDepth
             || type != XmlPullParser.END_TAG)) {
....
    final String name = parser.getName();
    if (TAG_ARGUMENT.equals(name)) {//如果argument标签
      inflateArgumentForDestination(res, dest, attrs, graphResId);
    } else if (TAG_DEEP_LINK.equals(name)) {//如果是deeplink标签
      inflateDeepLink(res, dest, attrs);
    } else if (TAG_ACTION.equals(name)) {//如果是action标签
      inflateAction(res, dest, attrs, parser, graphResId);
    } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {//如果是include标签并且目标是NavGraph
      final TypedArray a = res.obtainAttributes(
        attrs, androidx.navigation.R.styleable.NavInclude);
      final int id = a.getResourceId(
        androidx.navigation.R.styleable.NavInclude_graph, 0);
      ((NavGraph) dest).addDestination(inflate(id));
      a.recycle();
    } else if (dest instanceof NavGraph) {//如果目标是NavGraph
      ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
    }
  }

  return dest;
}

//解析argument
private void inflateArgumentForDestination(@NonNull Resources res, @NonNull NavDestination dest,
                                           @NonNull AttributeSet attrs, int graphResId) throws XmlPullParserException {
  final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavArgument);
  String name = a.getString(R.styleable.NavArgument_android_name);
  if (name == null) {
    throw new XmlPullParserException("Arguments must have a name");
  }
  NavArgument argument = inflateArgument(a, res, graphResId);//解析Argument
  dest.addArgument(name, argument);
  a.recycle();
}

//解析deeplink
private void inflateDeepLink(@NonNull Resources res, @NonNull NavDestination dest,
                             @NonNull AttributeSet attrs) throws XmlPullParserException {
  final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavDeepLink);
  String uri = a.getString(R.styleable.NavDeepLink_uri);//获取uri
  String action = a.getString(R.styleable.NavDeepLink_action);//获取action
  String mimeType = a.getString(R.styleable.NavDeepLink_mimeType);//获取类型
  if (TextUtils.isEmpty(uri) && TextUtils.isEmpty(action) && TextUtils.isEmpty(mimeType)) {
    throw new XmlPullParserException("Every <" + TAG_DEEP_LINK
                                     + "> must include at least one of app:uri, app:action, or app:mimeType");
  }
  NavDeepLink.Builder builder = new NavDeepLink.Builder();
  if (uri != null) {
    //设置UriPattern
    builder.setUriPattern(uri.replace(APPLICATION_ID_PLACEHOLDER,
                                      mContext.getPackageName()));
  }
  if (!TextUtils.isEmpty(action)) {
    //设置Action
    builder.setAction(action.replace(APPLICATION_ID_PLACEHOLDER,
                                     mContext.getPackageName()));
  }
  if (mimeType != null) {
    //设置类型
    builder.setMimeType(mimeType.replace(APPLICATION_ID_PLACEHOLDER,
                                         mContext.getPackageName()));
  }
  dest.addDeepLink(builder.build());
  a.recycle();
}

//解析action
private void inflateAction(@NonNull Resources res, @NonNull NavDestination dest,
                           @NonNull AttributeSet attrs, XmlResourceParser parser, int graphResId)
  throws IOException, XmlPullParserException {
  final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);
  final int id = a.getResourceId(R.styleable.NavAction_android_id, 0);
  final int destId = a.getResourceId(R.styleable.NavAction_destination, 0);
  NavAction action = new NavAction(destId);
	//对action标签参数进行解析与对NavOptions进行赋值
  NavOptions.Builder builder = new NavOptions.Builder();
  builder.setLaunchSingleTop(a.getBoolean(R.styleable.NavAction_launchSingleTop, false));
  builder.setPopUpTo(a.getResourceId(R.styleable.NavAction_popUpTo, -1),
                     a.getBoolean(R.styleable.NavAction_popUpToInclusive, false));
  builder.setEnterAnim(a.getResourceId(R.styleable.NavAction_enterAnim, -1));//进入动画
  builder.setExitAnim(a.getResourceId(R.styleable.NavAction_exitAnim, -1));//退出动画
  builder.setPopEnterAnim(a.getResourceId(R.styleable.NavAction_popEnterAnim, -1));//背景进入动画
  builder.setPopExitAnim(a.getResourceId(R.styleable.NavAction_popExitAnim, -1));//背景退出动画
  action.setNavOptions(builder.build());

  Bundle args = new Bundle();
  final int innerDepth = parser.getDepth() + 1;
  int type;
  int depth;
  ....
  if (!args.isEmpty()) {
    action.setDefaultArguments(args);//设置默认值
  }
  dest.putAction(id, action);
  a.recycle();
}

在NavController的setGraph方法中会调用NavInflater的inflate方法,该方法会返回一个NavGraph,NavGraph继承自NavDestination。在inflate方法中会解析xml文件的标签通过名字判断是哪一种类型的标签并调用相应的解析与赋值代码,如:如果是argument则执行inflateArgumentForDestination方法,如果是deeplink执行inflateDeepLink,action执行inflateAction方法,也就完成了对导航图的解析。

NavDestination中存有各个目地的信息,NavDestination被创建于Navigator的子类,如FragmentNavigator、ActivityNavigator,并且这些子类继承NavDestination实现自己的目标类。

小结

  • NavHostFragment作为导航的载体在Activity的layout布局或在代码中被引用
  • NavHostFragment中创建NavController对象
  • NavContoller对象将导航对象委托给Navigator
  • Navigator具体的导航、目标类创建等规则由子类实现。
  • NavInflater 负责解析Navgation文件,负责构建NavGraph导航图。
  • NavDestination 存有各个目的地信息,在FragmentNavigator和ActivityNavigator内部分别对应一个Destination类,该类继承NavDestination。
  • 在页面导航时,fragment的操作还是交由FragmentManager在操作,activity交由startActivity执行。

最后先放一个在网上找的一个UML图,虽然现在的版本与几年前的有所出入,但是大体的框架结构与思想还是没变。

image-20210418174029636

目前这个版本与UML图上的变化是:

  • SimpleNavigatiorProvider已经被NavigatorProvider所取代
  • Navigator的子类增加了一个DialogFragmentNavigator用于导航到DialogFragment

PS:有UML图后框架整体就会变的很清晰,有时间我也画一个UML图来加深一下印象吧。。。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值