V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
winlee28
V2EX  ›  Android

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

  •  
  •   winlee28 · 2020-06-09 13:32:17 +08:00 · 8846 次点击
    这是一个创建于 1630 天前的主题,其中的信息可能已经有所发展或是发生改变。

    源码解析

    谷歌推出 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 的源码也很简单,但是却涉及到很多的类,主要有以下几个:

    • Navigation 提供查找 NavController 方法
    • NavHostFragment 用于承载导航的内容的容器
    • NavController 通过 navigate 实现页面的跳转
    • Navigator 是一个 abstract,有是个主要实现类
    • NavDestination 导航节点
    • NavGraph 导航节点页面集合

    我们首先从 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 架构开发组件化应用实战

    2 条回复    2020-07-30 18:25:31 +08:00
    fromzero
        1
    fromzero  
       2020-06-12 10:14:55 +08:00
    这样改其实违背 nav 的设计思想,官方推荐的做法是保存 view 的各种状态,onCreateView 的时候恢复。
    rgxiao
        2
    rgxiao  
       2020-07-30 18:25:31 +08:00
    您的意思是把 Fragment 中的数据用 Activity 中的 VM 保存起来么?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1006 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 21:22 · PVG 05:22 · LAX 13:22 · JFK 16:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.