Android Navigation重建Fragment问题分析及解决
前言
最近项目中使用到了BottomNavigationView结合Navigation实现底部导航栏切换页面业务。
NavigationUI.setupWithNavController(bottomNavigationView, navController);
结果发现每次点击底部导航栏切换的时候都会重建Fragment,于是分析了源码,并研究了解决方案。
源码分析:
首先看布局文件:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toTopOf="@+id/bottom_nav_view" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/nav_graph" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav_view" android:layout_width="0dp" android:layout_height="50dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_menu" /> </androidx.constraintlayout.widget.ConstraintLayout>
当调用NavigationUI.setupWithNavController(BottomNavigationView, NavController)以后,setupWithNavController方法内部其实通过调用BottomNavigationView#setOnNavigationItemSelectedListener方法监听导航栏选中事件。
在BottomNavigationView.OnNavigationItemSelectedListener监听中,最终会调用到NavController#navigate方法,进入Navigation源码中。
Navigation源码分析
首先看NavHostFragment的执行流程。
1. NavHostFragment#onInflate
因为在xml中声明fragment因此,首先调用Fragment的onInflate方法。
@CallSuper @Override public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs, @Nullable Bundle savedInstanceState) { super.onInflate(context, attrs, savedInstanceState); final TypedArray navHost = context.obtainStyledAttributes(attrs, androidx.navigation.R.styleable.NavHost); final int graphId = navHost.getResourceId( androidx.navigation.R.styleable.NavHost_navGraph, 0); if (graphId != 0) { mGraphId = graphId; } navHost.recycle(); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment); final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false); if (defaultHost) { mDefaultNavHost = true; } a.recycle(); }
onInflate方法中主要是从XML属性中解析navGraph属性和defaultNavHost属性值。
2. NavHostFragment#onAttach
根据Fragment生命周期,然后执行的是onAttach方法。
@CallSuper @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (mDefaultNavHost) { getParentFragmentManager().beginTransaction() .setPrimaryNavigationFragment(this) .commit(); } }
onAttach方法中主要是设置NavHostFragment为导航器的主导航容器。
3. NavHostFragment#onCreate
@CallSuper @Override public void onCreate(@Nullable Bundle savedInstanceState) { final Context context = requireContext(); // 1. 实例化NavHostController mNavController = new NavHostController(context); mNavController.setLifecycleOwner(this); mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher()); mNavController.enableOnBackPressed( mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate); mIsPrimaryBeforeOnCreate = null; mNavController.setViewModelStore(getViewModelStore()); // 2. 创建DialogFragmentNavigator和FragmentNavigator并添加示例到NavigatorProvider中 onCreateNavController(mNavController); 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) { mNavController.restoreState(navState); } if (mGraphId != 0) { // 3. 设置导航配置文件 mNavController.setGraph(mGraphId); } else { 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); } } super.onCreate(savedInstanceState); }
onCreate方法中主要做三件事:
- 实例化NavHostController对象
- 创建DialogFragmentNavigator和FragmentNavigator并添加到NavHostController的父类NavController的NavigatorProvider类型的成员变量mNavigatorProvider中
- 调用NavHostController#setGraph方法设置导航配置文件nav_graph
public class NavHostController extends NavController { public NavHostController(@NonNull Context context) { super(context); } ... }
主要看父类初始化方法:
public class NavController { private NavigatorProvider mNavigatorProvider = new NavigatorProvider(); public NavController(@NonNull Context context) { ... mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider)); mNavigatorProvider.addNavigator(new ActivityNavigator(mContext)); } }
主要是创建NavGraphNavigator和ActivityNavigator实例并添加到NavController的成员变量mNavigatorProvider中。
4. NavHostFragment#onCreateNavController
@CallSuper protected void onCreateNavController(@NonNull NavController navController) { navController.getNavigatorProvider().addNavigator( new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); navController.getNavigatorProvider().addNavigator(createFragmentNavigator()); }
onCreate方法中调用了onCreateNavController方法添加DialogFragmentNavigator和FragmentNavigator示例。
5. NavigatorProvider
public class NavigatorProvider { private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>(); @NonNull static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) { String name = sAnnotationNames.get(navigatorClass); if (name == null) { // 自定义Navigator类的注解Navigator.Name Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class); name = annotation != null ? annotation.value() : null; ... sAnnotationNames.put(navigatorClass, name); } return name; } private final HashMap<String, Navigator<? extends NavDestination>> mNavigators = new HashMap<>() @Nullable public final Navigator<? extends NavDestination> addNavigator( @NonNull Navigator<? extends NavDestination> navigator) { String name = getNameForNavigator(navigator.getClass()); return addNavigator(name, navigator); } @CallSuper @Nullable public Navigator<? extends NavDestination> addNavigator(@NonNull String name, @NonNull Navigator<? extends NavDestination> navigator) { return mNavigators.put(name, navigator); } }
NavigatorProvider类内部主要是存储了键值为自定义Navigator时注解Navigator.Name指定的名称,值为对应的Navigator示例。
因此onCreate方法执行以后,NavigatorProvider中的mNavigators的值为:
("navigation", NavGraphNavigator) ("activity", ActivityNavigator) ("dialog", DialogFragmentNavigator) ("fragment", FragmentNavigator)
6. NavController#setGraph
@CallSuper public void setGraph(@NavigationRes int graphResId) { setGraph(graphResId, null); } @CallSuper public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs); } @CallSuper public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) { if (mGraph != null) { popBackStackInternal(mGraph.getId(), true); } mGraph = graph; onGraphCreated(startDestinationArgs); } @NonNull public NavInflater getNavInflater() { if (mInflater == null) { mInflater = new NavInflater(mContext, mNavigatorProvider); } return mInflater; }
这个方法中首先是实例化NavInflater并调用NavInflater#inflate解析导航配置文件,解析以后的结构存放在NavGraph类中。NavGraph是可以按ID获取的NavDestination节点的树形结构。
7. NavInflater#inflate
public final class NavInflater { private Context mContext; private NavigatorProvider mNavigatorProvider; public NavInflater(@NonNull Context context, @NonNull NavigatorProvider navigatorProvider) { mContext = context; mNavigatorProvider = navigatorProvider; } @NonNull public NavGraph inflate(@NavigationRes int graphResId) { ... NavDestination destination = inflate(res, parser, attrs, graphResId); ... } @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 int innerDepth = parser.getDepth() + 1; int type; int depth; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG) { continue; } if (depth > innerDepth) { continue; } final String name = parser.getName(); if (TAG_ARGUMENT.equals(name)) { inflateArgumentForDestination(res, dest, attrs, graphResId); } else if (TAG_DEEP_LINK.equals(name)) { inflateDeepLink(res, dest, attrs); } else if (TAG_ACTION.equals(name)) { inflateAction(res, dest, attrs, parser, graphResId); } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) { final TypedArray a = res.obtainAttributes( attrs, androidx.navigation.R.styleable.NavInclude); final int id = a.getResourceId( androidx.navigation.R.styleable.NavInclude_graph, 0); ((NavGraph) dest).addDestination(inflate(id)); a.recycle(); } else if (dest instanceof NavGraph) { ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId)); } } return dest; } ... }
NavInflater的主要工作就是解析导航配置文件。接下来再回头看setGraph方法中调用的onGraphCreated方法。
8. NavController#onGraphCreated
private void onGraphCreated(@Nullable Bundle startDestinationArgs) { ... if (mGraph != null && mBackStack.isEmpty()) { boolean deepLinked = !mDeepLinkHandled && mActivity != null && handleDeepLink(mActivity.getIntent()); if (!deepLinked) { // Navigate to the first destination in the graph // if we haven't deep linked to a destination navigate(mGraph, startDestinationArgs, null, null); } } else { dispatchOnDestinationChanged(); } }
刚开始的时候会执行到navigate方法。
9. NavController#navigate
private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { boolean popped = false; boolean launchSingleTop = false; if (navOptions != null) { if (navOptions.getPopUpTo() != -1) { popped = popBackStackInternal(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive()); } } Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( node.getNavigatorName()); Bundle finalArgs = node.addInDefaultArgs(args); NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); ... }
根据分析得出getNavigator获取到的Navigator是NavGraphNavigator实例。
10. NavGraphNavigator#navigate
@Nullable @Override public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) { int startId = destination.getStartDestination(); if (startId == 0) { throw new IllegalStateException("no start destination defined via" + " app:startDestination for " + destination.getDisplayName()); } NavDestination startDestination = destination.findNode(startId, false); if (startDestination == null) { final String dest = destination.getStartDestDisplayName(); throw new IllegalArgumentException("navigation destination " + dest + " is not a direct child of this NavGraph"); } Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( startDestination.getNavigatorName()); return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args), navOptions, navigatorExtras); }
navigate方法中通过startId找到NavDestination变量,再根据NavDestination#getNavigatorName方法获取到的名称得到对应的Navigator实例,此处获取到的是FragmentNavigator实例。
11. FragmentNavigator#navigate
@Nullable @Override public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { ... 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; } }
navigate方法中使用的是FragmentFactory(反射)创建fragment实例。最后通过FragmentTransaction#replace方法添加fragment实例。
再回头看NavHostFragment的生命周期onCreateView方法。
11. NavHostFragment#onCreateView
@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { FragmentContainerView containerView = new FragmentContainerView(inflater.getContext()); // When added via XML, this has no effect (since this FragmentContainerView is given the ID // automatically), but this ensures that the View exists as part of this Fragment's View // hierarchy in cases where the NavHostFragment is added programmatically as is required // for child fragment transactions containerView.setId(getContainerId()); return containerView; }
NavHostFragment默认展示视图是FragmentContainerView。
12. NavHostFragment#onViewCreated
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Navigation.setViewNavController(view, mNavController); // When added programmatically, we need to set the NavController on the parent - i.e., // the View that has the ID matching this NavHostFragment. if (view.getParent() != null) { mViewParent = (View) view.getParent(); if (mViewParent.getId() == getId()) { Navigation.setViewNavController(mViewParent, mNavController); } } }
public static void setViewNavController(@NonNull View view, @Nullable NavController controller) { view.setTag(R.id.nav_controller_view_tag, controller); }
private static NavController findViewNavController(@NonNull View view) { while (view != null) { NavController controller = getViewNavController(view); if (controller != null) { return controller; } ViewParent parent = view.getParent(); view = parent instanceof View ? (View) parent : null; } return null; }
private static NavController getViewNavController(@NonNull View view) { Object tag = view.getTag(R.id.nav_controller_view_tag); NavController controller = null; if (tag instanceof WeakReference) { controller = ((WeakReference<NavController>) tag).get(); } else if (tag instanceof NavController) { controller = (NavController) tag; } return controller; }
将NavController设置为NavHostFragment的根视图View的tag,以后调用Navigation#findNavController时, 会从传入视图及其所有父视图中找tag,直到找到NavController为止。
从以上分析可以看出,每次调用NavController#navigate方法都会重新生成一个新的Fragment并且调用FragmentTransaction#replace添加,所以每次都会看到重建Fragment的现象。
解决方案
自定义Navigator重写Navigator#navigate方法。
@Navigator.Name("customNavigator") public class CustomNavigator extends FragmentNavigator { private Context context; private FragmentManager fragmentManager; private int containerId; public CustomNavigator(@NonNull Context context, @NonNull FragmentManager fragmentManager, int containerId) { super(context, fragmentManager, containerId); this.context = context; this.fragmentManager = fragmentManager; this.containerId = containerId; } @Nullable @Override public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) { FragmentTransaction ft = fragmentManager.beginTransaction(); // 获取当前显示的Fragment Fragment fragment = fragmentManager.getPrimaryNavigationFragment(); if (fragment != null) { ft.hide(fragment); } final String tag = String.valueOf(destination.getId()); fragment = fragmentManager.findFragmentByTag(tag); if (fragment != null) { ft.show(fragment); } else { fragment = instantiateFragment(context, fragmentManager, destination.getClassName(), args); ft.add(containerId, fragment, tag); } ft.setPrimaryNavigationFragment(fragment); ft.setReorderingAllowed(true); ft.commit(); return destination; } }
然后通过NavigatorProvider添加即可:
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); navController.getNavigatorProvider().addNavigator(new CustomNavigator(this, getSupportFragmentManager(), R.id.nav_host_fragment));
到此这篇关于Android Navigation重建Fragment问题分析及解决的文章就介绍到这了,更多相关Android Navigation 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
React-Native Android 与 IOS App使用一份代码实现方法
这篇文章主要介绍了React-Native Android 与 IOS App使用一份代码实现方法的相关资料,这里举例说明,该如何实现IOS和Android APP 都使用一样的代码,需要的朋友可以参考下2016-12-12Android开发之ToggleButton实现开关效果示例
这篇文章主要介绍了Android开发之ToggleButton实现开关效果的方法,结合实例形式分析了ToggleButton控件实现开关效果的布局与功能相关操作技巧,需要的朋友可以参考下2017-07-07Android使用友盟集成QQ、微信、微博等第三方分享与登录方法详解
之前的项目第三方分享和登录一直都使用ShareSDK实现的。为了统一使用友盟的全家桶,所以三方分享和登录也就选择了友盟,这里为大家整理出详细方法2018-03-03Android ListView构建支持单选和多选的投票项目
如何在Android的ListView中构建CheckBox和RadioButton列表?这篇文章主要为大家详细介绍了Android ListView实现支持单选和多选的投票项目,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2017-01-01
最新评论