Navigation作为Jetpack四大组件之一,可以为单Activity多Fragment结构提供重要支持。我们知道Activity是属于比较重的组件,而Fragment是比较轻量化的,因此这种结构对界面性能方面有很大影响
但是每种框架都有他的优缺点,Navigation对外宣传时的确优点很多,比如fragment各种状态管理,参数类型限制,快速切换fragment等,当然这种也是吸引我们尝试这种框架的原因,不过有些特点只有在使用过程中才会发现,这也是为什么我们经常要做预研的原因,只有充分了解框架特性,我们才能决定适不适合引入以及怎么引入,所以这边初步总结一下使用过程中遇到的Navigation特性来作为大家是否引入的一个参考
1)和Gilde一样,一句话代码实现功能,如下:
NavHostFragment.findNavController(this@PanelHomeFragment).navigate(R.id.action_panelHomeFragment_to_creatorFilterFragment, generateArguments())2)接着深入navigate函数:
//...此处省略一大堆代码,直接进入主题 Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( node.getNavigatorName()); Bundle finalArgs = node.addInDefaultArgs(args); NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras);上面是Navigation跳转的核心代码,省略代码大多是它对自身栈的维护逻辑,这边就不详细介绍了。可以看到上面代码通过mNavigatorProvider来获取一个Navigator对象,然后通过它来实现跳转,这个方法传了一个node.getNavigatorName()参数,这个参数哪来的呢,要解释这个我们先来看一下Navigation跳转定义代码,如下:
<fragment android:id="@+id/filterFragment" android:name="com.test.fragment.FilterFragment" android:label="FilterFragment" />上面就是答案了,<fragment/>标签名就是这个参数了,所以最终我们拿到的Navigator对象就是FragmentNavigator
3)焦点转到FragmentNavigator来看看:
@Navigator.Name("fragment") public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> { private static final String TAG = "FragmentNavigator"; private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds"; @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; } 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.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; } } }先提示一下上一步说的node.getNavigatorName为什么对应这个对象呢,注意看一下@Navigator.Name("fragment")这个注解,这个就是答案,Navigation通过这个值来注册FragmentNavigator,所以根据fragment标签获取到的Navigator就是FragmentNavigator,这里其实还包含了一个自定义功能,后面再介绍
重点看一下navigate函数吧,可以看到里面就是我们平时用到的fragment切换代码,只不过这边比较特别,是用的getChildFragmentManager()加replace方式替换fragment,如果对这两个特性了解的同学应该知道这意味着:
每次fragment切换都是销毁前一个对象的,所以不管是回退还是跳转下一个,fragment都会重新执行oncreateView等生命周期流程因为基于getChildFragmentManager(),所以Navigation里面的fragment都是属于NavHostFragment的子fragment,这个特性后面也会用到
问题:
每次切换fragment都会重新创建,对于新项目或者新功能而言是可以接受的,但是如果要混合一些旧代码你就要三思了,因为销毁fragment同时意味着数据全部消失,而旧代码基本是不会缓存数据的,如果盲目使用只会中途左右为难
解决方案:
a) 坚决拥护Navigation设计初衷,即UI与数据分离,利用viewModel作为数据缓存工具,配合Navigation使用,这样可以解决数据丢失问题。如果不想回退的时候也重绘UI,那么根据google官方推荐可以保存view对象,进行判重:
b) 当然如果我非要用Navigation,同时又不想每次重新创建呢,答案当然也是可以的,不过这种方式个人感觉有点背离Navigation设计初衷,多个fragment同时存在对于内存是挺大压力的,特别像主页多面板这种还容易造成OOM,另外每次刷新UI对用户体验其实影响不大,最耗时的其实是数据获取,只要我们缓存了数据就不会有太大问题。同时刷新UI也会及时刷新数据,这方面来看反而对体验有帮助。
说了一堆劝阻大家不要改动Naviagtion的理由,最后还是得介绍一下怎样改动它来实现多个fragment共存。首先fragment不能共存的原因是因为使用了replace方式,那换成add不就行了(^_^),既然有思路那就简单了,那怎么换呢,又不能改它的代码。
别急,答案其实在上面逐步透漏了,还记得我们跳转为什么是进来FragmentNavigator而不是其他对象吗,因为Navigation只注册了一个FragmentNavigator给我们用,既然我们要改跳转逻辑,那就自己注册一个不就行了,说干就干,怎么注册呢?
首先定义一个和FragmentNavigator对象类似的(把它代码全拷过来,改个名字),然后把@Navigator.Name("fragment")这个换成我们自定义标签,比如state_fragment定义好了怎么告诉Navigation呢,先来看一下FragmentNavigator怎么添加的吧: @CallSuper protected void onCreateNavController(@NonNull NavController navController) { navController.getNavigatorProvider().addNavigator( new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); navController.getNavigatorProvider().addNavigator(createFragmentNavigator()); }很简单的几行代码,那我们可以这样添加吗?当然是可以的,因为我们外面是可以拿到navController的,自然也能像它一样操作了
经过上面两步,基本差不多了,接下来怎么用呢,很简单,在navigation.xml把所有<fragment/>标签换成<state_fragment/>就行了,如下: <state_fragment android:id="@+id/filterFragment" android:name="com.test.fragment.FilterFragment" android:label="FilterFragment" />上面就是如何自定义跳转的主要过程了,虽然提供了方法,但是个人还是建议保持架构原本特性,毕竟google也是选了最适合的方式来作为默认方式,改造了之后后面升级维护也是一笔开销。
反过来,如果提前知道Navigation这些特性,就能在架构设计时就做出正确决定,或者在兼容旧代码时自定义不销毁的fragment就行了,能少走很多弯路
经过上面对Navigation的初步实践总结,我们发现在考虑使用Navigation的同时要考虑数据缓存的接入,两者是相辅相成的,缺一不可,而目前比较推荐的数据缓存框架就是ViewModel了,但是这个使用起来也是有一些坑的,需要提前了解才行!
按照我们对viewModel基本了解,viewModel是连接UI与数据的桥梁,因此需要定义一个LiveData来通知UI进行相关处理,如下:
/** * uiState作为内部交互通道 */ protected val _uiState = MutableLiveData<EventModel>() val uiState: LiveData<EventModel> get() = _uiState protected val uiModel = EventModel("", "")然后fragment只要监听uiState就能收到消息了,想想感觉挺简单的呀,这样就结束了?那是不现实的,这样会遇到几个问题:
uiState作为数据交互桥梁,传递数据给fragment这点没错,但是除了数据还有其他事件也要传递的,而viewModel是不保存中间状态的,也就是数据会被覆盖,那说好的缓存呢?既然一个uiState不够,再来一个不就行了,简单嘛,如下: /** * dataMode主要作为缓存所有列表数据 */ private val dataMode = EventModel("", "") private val _dataState = MutableLiveData<EventModel>() val dataState: LiveData<EventModel> get() = _dataState 好了,数据覆盖问题被解决了。真的解决了吗?viewModel生命周期是跟随谁这个有考虑过吗,activity or fragment?思前想后,只能跟activity了,不然fragemnt一销毁不是一无所有,何谈缓存数据。再问自己一下,如果activity是MainActivity,这样做好吗,虽然viewModel不会销毁了,但是什么时候释放呢,答案是基本上不会释放,除非退后台一段时间,要哭了(^_^)最后揭晓答案,还记得之前提到的Navigation切换fragment用的是childFragmentManager,了解的同学是不是恍然大悟了,原来所有的fragment的父fragment都是NavHostFragment,那就简单了,跟随它的生命周期不就行了,于是憋出了一行代码: override fun initVM(): StickerViewModel = ViewModelProvider(requireParentFragment())[StickerViewModel::class.java]
考虑一个问题:fragment每次都是重新创建的,那是不是每次都要重新注册监听?那注册的时候LiveData是不是很友好的把上一次的数据发给你了?
是不是突然发现了,fragment退出重进是会收到上一次遗留消息的,如果不注意可能找bug要找半天,那说了这么多怎么解决呢?其实也不难,在基类fragment里面清除状态就行了,如下:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mViewModel = initVM() if (isNeedClearState()) { mViewModel.clearUIState() } getPanelType()?.let { PanelHostViewModel.get().register(it, mViewModel) } initView() initData() startObserve() super.onViewCreated(view, savedInstanceState) }那是不可能的,答案就是:
Navigation虽然会销毁fragment,但是回退的fragment各种变量值还是在的(这也是能复用view的原因)当你收到arguments进行跳转到下一个fragment的时候,再点击返回键,这时候上一个fragment(收到参数的那个)又执行了初始化操作,发现arguments参数还在,那怎么办,继续跳转呗,这不又到了下一个fragment,所以无论你把返回键点爆了还是怎么的,它就是回不去了,有种你杀进程呀~那怎么办呢,也很简单,同样在基类fragment里面销毁时清空argument,附上完整基类代码: /** * 需要用到ViewModel的Fragment继承这个类 */ abstract class BaseVMFragment<VM : BaseViewModel> : Fragment() { protected var mContentView: View? = null protected lateinit var mViewModel: VM override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (mContentView == null) { mContentView = inflater.inflate(getLayoutResId(), container, false) } return mContentView } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mViewModel = initVM() if (isNeedClearState()) { mViewModel.clearUIState() } getPanelType()?.let { PanelHostViewModel.get().register(it, mViewModel) } initView() initData() startObserve() super.onViewCreated(view, savedInstanceState) } override fun onDestroyView() { //需要清除参数和uiState,否则下次从panelHome跳转过来viewModel会收到change_layer消息,从stickerFragment返回会重新应用jumpPanelType,导致异常 arguments?.clear() super.onDestroyView() } open fun isNeedClearState(): Boolean { return true } abstract fun getLayoutResId(): Int abstract fun getPanelType(): PanelType? abstract fun initVM(): VM abstract fun initView() abstract fun initData() abstract fun startObserve() }
从上面介绍可以看出,单单从Google官网或者网上一些介绍,我们只能了解到Navigation使用的一些基本要求以及优缺点,但是真要应用到项目里,特别是和旧代码混合使用就要慎重了,不然中途会左右为难,放弃吧,又投入了这么多时间精力。继续搞吧,又搞不下去了。
因此这篇文章也是帮助大家避雷,毕竟基于足够的了解才能做出正确的决策,相信大家在看了上面介绍后,使用的时候会提前思考多种情况,如果都满足的话大概率能顺利融合Navigation,避免中途的尴尬处境!
