背景
上一篇文章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