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歡迎關注公共號
關注公共號會有更多收穫!
