Navigation修改版-避免生命周期重复回调

3,837 阅读5分钟

本文首发于微信公众号「Android开发之旅」,欢迎关注 ,获取更多技术干货

Jetpack版Wan-Android项目地址:Android Jetpack架构开发组件化应用实战 欢迎star

Flutter版Wan-Android项目地址:Flutter版Wan-Android 欢迎star

源码解析

谷歌推出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架构开发组件化应用实战

扫描下方二维码关注公众号,获取更多技术干货。