Navigation 源码解析及自定义 FragmentNavigator 详解

2020-06-09 13:32:17 +08:00
 winlee28

源码解析

谷歌推出 Navigation 主要是为了统一应用内页面跳转行为。本文主要是根据 Navigation 版本为 2.1.0 的源码进行讲解。

'androidx.navigation:navigation-fragment:2.1.0' 
'androidx.navigation:navigation-ui:2.1.0'           
'androidx.navigation:navigation-fragment-ktx:2.1.0'            'androidx.navigation:navigation-ui-ktx:2.1.0'

Navigation 的使用很简单,在创建新项目的时候可以直接选择 Bottom Navigation Activity 项目,这样默认就已经帮我们实现了相关页面逻辑。

之前写过 Navigation 相关的用法,请移步:

Android Jetpack 架构组件 — Navigation 入坑详解

Navigation 的源码也很简单,但是却涉及到很多的类,主要有以下几个:

我们首先从 NavHostFragment 入手查看,因为他是直接定义在我们的 XML 文件中的,我们直接查看器生命周期方法 onCreate:

		@CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavHostController(context); //1
        mNavController.setLifecycleOwner(this);
     		
      	....
      	
      	onCreateNavController(mNavController);//2

      	....
    }

注释 1 处 直接创建了 NavHostController 并通过 findNavController 方法暴露给外部调用者。NavHostController 是继承自 NavController 的。注释 2 处代码如下:

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

通过 navController 获取 NavigatorProvider 并向其中添加了两个 Navigator,分别为 DialogFragmentNavigator 和 FragmentNavigator 。另外在 NavController 的构造方法中还添加了另外两个 Navigator,如下:

public NavController(@NonNull Context context) {
    ....
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}

他们都是 Navigator 的实现类。分别对应于 DialogFragment 、Fragment 和 Activity 的页面跳转。大家可能对于 NavGraphNavigator 一些好奇,它是用在什么地方的呢? 其实我们在 XML 中配置的 navGraph 对应的 navigation 跟节点文件中的 startDestination 就是通过 NavGraphNavigator 来实现跳转的。这也是它目前唯一的用途。

各个 Navigator 通过复写 navigate 方法来实现各自的跳转逻辑。这里重点强调下 FragmentNavigator 的实现逻辑:

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
   
  	....
  
    final Fragment frag = instantiateFragment(mContext, mFragmentManager,
            className, args);
    frag.setArguments(args);
    final FragmentTransaction ft = mFragmentManager.beginTransaction();

    ....

    ft.replace(mContainerId, frag); //1
    
  	....
}

最关键的一行代码就是注释 1 处。他是通过 replace 来加载 Fragment 的 ,这不符合我们实际的开发逻辑。文章后续会讲解如何自定义 FragmentNavigator 来避免 Fragment 在切换的时候 生命周期的执行。

回到上文中的 navController 获取的 NavigatorProvider 其内部是维护了一个 HashMap 来存储相关的 Navigator 信息。通过获取到 Navigator 的注解 Name 为 key 和 Navigator 的 getClass 为 value 进行存储。

我们在回到上文中的 onCreate 方法:

@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final Context context = requireContext();
	
 		....
  
    if (mGraphId != 0) {
        mNavController.setGraph(mGraphId);
    } else {
      
      	....	
      
        if (graphId != 0) {
            mNavController.setGraph(graphId, startDestinationArgs);
        }
    }
}

这里通过 mNavController 调用了 setGraph 。这里主要是为了解析我们的 XML 中配置的 mobile_navigation 节点信息文件。会根据不同的节点来各自解析。

@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 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 节点
            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) { // NavGraph 节点
            ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
        }
    }

    return dest;
}

通过获取 NavInflater 来对其进行解析。解析后返回 NavGraph,NavGraph 是继承自 NavDestination 的。里面主要是保存了所有解析出来的节点信息。

最后简单的总结下就是通过 NavHostFragment 获取到 NavContorl 并存储了相关的 Navigator 信息。通过各自的 navigate 方法进行页面的跳转。通过 setGraph 来解析配置的页面节点信息,并封装为 NavGraph 对象。里面通过 SparseArray 来存储 Destination 信息。

自定义 FragmentNavigator

上文中我们说了需要自定义自己的 Navigator 用于承载 Fragment 。主要的实现思路就是继承现有的 FragmentNavigator 并复写其 navigate 方法,将其中的 replace 方法 替换为 show 和 hide 方法 来完成 Fragment 的切换。

那么我们自定义的 Navigator 如何才能让系统识别呢? 这也简单,只要给我们的 类加上注解 @Navigator.Name(value) 那么他就是一个 Navigator 了。最后通过上文中分析的思路 在将其加入到 NavigatorProvider 中 即可。

具体的自定义 Navigator 已经在项目 Android Jetpack 架构开发组件化应用实战 中了,类名:FixFragmentNavigator 。大家可以自行去看下。这里就将核心的代码贴出来看下:

@Navigator.Name("fixFragment") //新的 Navigator 名称
class FixFragmentNavigator(context: Context, manager: FragmentManager, containerId: Int) :
    FragmentNavigator(context, manager, containerId) {
      
    override fun navigate(
            destination: Destination,
            args: Bundle?,
            navOptions: NavOptions?,
            navigatorExtras: Navigator.Extras?
        ): NavDestination? {

            .... 

            //ft.replace(mContainerId, frag)

            /**
             * 1 、先查询当前显示的 fragment 不为空则将其 hide
             * 2 、根据 tag 查询当前添加的 fragment 是否不为 null,不为 null 则将其直接 show
             * 3 、为 null 则通过 instantiateFragment 方法创建 fragment 实例
             * 4 、将创建的实例添加在事务中
             */
            val fragment = mManager.primaryNavigationFragment //当前显示的 fragment
            if (fragment != null) {
                ft.hide(fragment)
            }

            var frag: Fragment?
            val tag = destination.id.toString()
            frag = mManager.findFragmentByTag(tag)
            if (frag != null) {
                ft.show(frag)
            } else {
                frag = instantiateFragment(mContext, mManager, className, args)
                frag.arguments = args
                ft.add(mContainerId, frag, tag)
            }

            ....
        }
}

自定义完成好,还需要将 mobile_navigation 的节点中远 fragment 替换为 fixFragment 节点。并删除布局文件中 NavHostFragment 节点的

app:navGraph="@navigation/mobile_navigation"

信息,因为我们需要手动将 FixFragmentNavigator 和 NavControl 进行关联。

//添加自定义的 FixFragmentNavigator
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
val fragment =
    supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val fragmentNavigator =
    FixFragmentNavigator(this, supportFragmentManager, fragment!!.id)
navController.navigatorProvider.addNavigator(fragmentNavigator)

navController.setGraph(R.navigation.mobile_navigation)

这样就完成了自定义 Navigator 实现切换 Tab 的时候 Fragment 生命周期不会重新执行了。

具体代码逻辑详见:Android Jetpack 架构开发组件化应用实战

9406 次点击
所在节点    Android
2 条回复
fromzero
2020-06-12 10:14:55 +08:00
这样改其实违背 nav 的设计思想,官方推荐的做法是保存 view 的各种状态,onCreateView 的时候恢复。
rgxiao
2020-07-30 18:25:31 +08:00
您的意思是把 Fragment 中的数据用 Activity 中的 VM 保存起来么?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/679969

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX