Navigation导航重走Fragment生命周期问题

1,259 阅读7分钟

谷歌推出Navigation主要是为了统一应用内页面跳转行为。

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

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

navigation三要素:

  1. navigation graph(xml resource): 可视化的navigation 资源文件
  2. NavHostFragment:当前Fragment容器
  3. NavController: //导航控制者

Navigation简单使用

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navView: BottomNavigationView = findViewById(R.id.nav_view) //1.底部导航View

        val navController = findNavController(R.id.nav_host_fragment) //2.导航控制者
        navView.setupWithNavController(navController)
    }
}

我们首先从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);
        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);   //2

        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);
            }
        }
    }

步骤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 的实现逻辑:

 @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);//关键代码:使用replase替换
        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;
        }
    }

最关键的一行代码就是注释处。他是通过 replace 来加载 Fragment 的 ,这就会导致导航切换时重走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 信息。

回到我们上面说的问题,如何解决页面切换导致的Fragment生命周期重走的问题,自定义FragmentNavigator。

自定义FragmentNavigator

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

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

@Navigator.Name("fragment")  //此处名字必须和FragmentNavigator中的名字一致,不然不起作用
class FixFragmentNavigator(context: Context, manager: FragmentManager, conditionId: Int) : FragmentNavigator(context, manager, conditionId) {
    private val mContext = context
    private val mManager = manager
    private val mContainerId = conditionId

    private val TAG  = "FixFragmentNavigator"

    override fun navigate(destination: Destination,
                          args: Bundle?,
                          navOptions: NavOptions?,
                          navigatorExtras: Navigator.Extras?):
            NavDestination? {

        Log.d(TAG,"124454546456")
        if (mManager.isStateSaved) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state")

            return null
        }
        var className = destination.className
        if (className[0] == '.') {
            className = mContext.packageName + className
        }
        val ft = mManager.beginTransaction()

        var enterAnim = navOptions?.enterAnim ?: -1
        var exitAnim = navOptions?.exitAnim ?: -1
        var popEnterAnim = navOptions?.popEnterAnim ?: -1
        var popExitAnim = navOptions?.popExitAnim ?: -1
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = if (enterAnim != -1) enterAnim else 0
            exitAnim = if (exitAnim != -1) exitAnim else 0
            popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
            popExitAnim = if (popExitAnim != -1) popExitAnim else 0
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
        }

//        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)
        }

        ft.setPrimaryNavigationFragment(frag)

        @IdRes val destId = destination.id


        /**
         *  通过反射的方式获取 mBackStack
         */
        val mBackStack: ArrayDeque<Int>

        val field = androidx.navigation.fragment.FragmentNavigator::class.java.getDeclaredField("mBackStack")
        field.isAccessible = true
        mBackStack = field.get(this) as ArrayDeque<Int>


        val initialNavigation = mBackStack.isEmpty()
        // TODO Build first class singleTop behavior for fragments
        val isSingleTopReplacement = (navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId)

        val isAdded: Boolean
        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
                mManager.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 is Extras) {
            val extras = navigatorExtras as Extras?
            for ((key, value) in extras!!.sharedElements) {
                ft.addSharedElement(key, value)
            }
        }
        ft.setReorderingAllowed(true)
        ft.commit()
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId)
            return destination
        } else {
            return null
        }
    }

    /**
     * 在父类是 private的  直接定义一个方法即可
     */
    private fun generateBackStackName(backIndex: Int, destid: Int): String {
        return "$backIndex - $destid"
    }

}

此处需要注意的点:@Navigator.Name("fragment") 此处名字必须和FragmentNavigator中的名字一致,不然不起作用,上面提到存储Nacitator的是Map集合,只有名字相同才能覆盖初始添加的FragmentNavigator。

自定义完成好,需要删除布局文件中NavHostFragment的节点引用

 <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/release_navigation" />  //把此节点删除

在代码中手动将 FixFragmentNavigator 和 NavControl 进行关联。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navView: BottomNavigationView = findViewById(R.id.nav_view) //1.底部导航View

        val navController = findNavController(R.id.nav_host_fragment) //2.导航控制者

        //自定义Navigator
        //1. 创建NavHostFragment
        val fragment  = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        //2. 创建自定义navigator
        val navigator = FixFragmentNavigator(this,supportFragmentManager,fragment!!.id)
        //3. 添加navigator
        navController.navigatorProvider.addNavigator(navigator)
        //4. 设置xml导航
        navController.setGraph(R.navigation.mobile_navigation)
        navView.setupWithNavController(navController)
    }
}

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

此处参考了Navigation修改版-避免生命周期重复回调

谢谢作者的帮助。