Navigation——Fragment创建新的实例问题详解

背景

上一篇文章Navigation——Fragment创建新的实例问题,我们简述了我们在使用Navigation遇到的Fragment创建新的实例的问题。接下来,我们在这篇文章就来解决一下我们遇到的这个问题

源码追踪

打开 MainActivity 的布局文件,我们可以看到在布局文件当中, Frangmet 这里,有一个来自于 androidx的NavHostFragment。

   <fragment
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/my_nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/nav_graph"
            app:defaultNavHost="true"
    />

进入 NavHostFragment 的源码,我们一探究竟。 在 NavHostFragment 源码的 onCreate 方法当中,我们找到了答案。 完整的 onCreat 方法

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

        mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());

        Bundle navState = null;
        if (savedInstanceState != null) {
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                mDefaultNavHost = true;
                requireFragmentManager().beginTransaction()
                        .setPrimaryNavigationFragment(this)
                        .commit();
            }
        }

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

在这其中,有一行

 mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());

也就是说,只要添加一个 Fragment 就会在 NavController 当中 Add 一个 FragmentNavigator ,而在 createFragmentNavigator 方法当中,Navigator 方法里对 Fragment 进行了处理

    @NonNull
    protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
        return new FragmentNavigator(requireContext(), getChildFragmentManager(), getId());
    }

解决问题

既然,我们都已经找到导致没次都创建新的实例的根结所在,那么我们现在来解决一下问题。1那么我们只需要重新写一个NavHostFragment的createFragmentNavigator的方法,来满足我们的要求。

/**
 * 复用的NavHostFragment (默认不是复用 引起一个问题就是 不保存fragment状态)
 */
class TabNavHostFragment : NavHostFragment() {
    override fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> {
        return MyNavigator(requireContext(), childFragmentManager, id)
    }

    //参考相关链接
    // https://stackoverflow.com/questions/50485988/is-there-a-way-to-keep-fragment-alive-when-using-bottomnavigationview-with-new-n/51684125
    @Navigator.Name("tab_fragment")  // 这个名称在 navigation.xml 当中使用。
    open class MyNavigator(var mContext: Context, var mFragmentManager: FragmentManager, var mContainerId: Int) :
        FragmentNavigator(mContext, mFragmentManager, mContainerId) {
        override fun navigate(
            destination: Destination,
            args: Bundle?,
            navOptions: NavOptions?,
            navigatorExtras: Navigator.Extras?
        ): NavDestination? {
            try {
                //反射获取mBackStack mIsPendingBackStackOperation
                val mBackStackField = FragmentNavigator::class.java.getDeclaredField("mBackStack")
                mBackStackField.isAccessible = true
                var mBackStack: ArrayDeque<Int> = mBackStackField.get(this) as ArrayDeque<Int>

                val mIsPendingBackStackOperationField =
                    FragmentNavigator::class.java.getDeclaredField("mIsPendingBackStackOperation")
                mIsPendingBackStackOperationField.isAccessible = true
                var mIsPendingBackStackOperation: Boolean = mIsPendingBackStackOperationField.get(this) as Boolean

                if (mFragmentManager.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 = mFragmentManager.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)
                }

                val tag = destination.id.toString()
                //ft.replace(mContainerId, frag)

                val currentFragment = mFragmentManager.primaryNavigationFragment
                if (currentFragment != null) {
                    ft.hide(currentFragment)
                }

                var frag = mFragmentManager.findFragmentByTag(tag)
                if (frag == null) {
                    frag = instantiateFragment(
                        mContext, mFragmentManager,
                        className, args
                    )
                    frag.arguments = args
                    ft.add(mContainerId, frag, tag)
                } else {
                    ft.show(frag)
                }

                ft.setPrimaryNavigationFragment(frag)

                @IdRes val destId = destination.id
                val initialNavigation = mBackStack.isEmpty()
                // TODO Build first class singleTop behavior for fragments
                val isSingleTopReplacement = (navOptions != null && !initialNavigation
                        && navOptions.shouldLaunchSingleTop()
                        && mBackStack.peekLast().toInt() == 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
                        mFragmentManager.popBackStack(
                            generateMyBackStackName(mBackStack.size, mBackStack.peekLast()),
                            FragmentManager.POP_BACK_STACK_INCLUSIVE
                        )
                        ft.addToBackStack(generateMyBackStackName(mBackStack.size, destId))
                        mIsPendingBackStackOperation = true
                        mIsPendingBackStackOperationField.set(this, true)
                    }
                    isAdded = false
                } else {
                    ft.addToBackStack(generateMyBackStackName(mBackStack.size + 1, destId))
                    mIsPendingBackStackOperation = true
                    mIsPendingBackStackOperationField.set(this, true)
                    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
                return if (isAdded) {
                    mBackStack.add(destId)
                    destination
                } else {
                    null
                }
            } catch (e: Throwable) {
                return super.navigate(destination, args, navOptions, navigatorExtras)
            }
        }

        private fun generateMyBackStackName(backStackIndex: Int, destId: Int): String {
            return "$backStackIndex-$destId"
        }
    }
}

然后,在我们的代码当中,引入我们自定修改之后的这个 TabNavHostFragment 在 MianActivity 的布局文件当中修改为

<fragment
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/my_nav_host_fragment"
            android:name="com.demo.navigationcomponent.TabNavHostFragment"
            app:navGraph="@navigation/nav_graph"
            app:defaultNavHost="true"
    />

然后,在 nav_graph.xml文件当中,修改为:

    <tab_fragment android:id="@+id/oneFragment" android:name="com.demo.navigationcomponent.OneFragment"
              android:label="fragment_one" tools:layout="@layout/fragment_one">
        <action android:id="@+id/action_oneFragment_to_twoFragment"
                app:destination="@id/twoFragment"
                app:popUpTo="@id/oneFragment"
                app:popUpToInclusive="true"/>
    </tab_fragment>

至此,我们大功告成了,当我们在添加新的 Fragment 的时候,当已经创建过 Fragment 的实例的时候,就不会创建新的实例了。

最后

通过以上的方法,可以实现我们想要的效果,但是我认为这只是一个临时的解决方案,修改源码这种方式,并不是一个特别好的解决方案。如果有其他更好的方法,方案,欢迎给我公共号「朝阳杨大爷」给我留言,讨论。

GitHub 地址

代码,我已经放到了 GitHub 上了欢迎下载 Star https://github.com/yang0range/NavigationComponent/tree/Branch_One

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券