当前位置 : 主页 > 手机开发 > android >

Android Navigation重建Fragment问题分析及解决

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 前言 Navigation源码分析 1. NavHostFragment#onInflate 2. NavHostFragment#onAttach 3. NavHostFragment#onCreate 4. NavHostFragment#onCreateNavController 5. NavigatorProvider 6. NavController#setGraph 7. NavInflater#inflate 8. NavC
目录
  • 前言
  • Navigation源码分析
    • 1. NavHostFragment#onInflate
    • 2. NavHostFragment#onAttach
    • 3. NavHostFragment#onCreate
    • 4. NavHostFragment#onCreateNavController
    • 5. NavigatorProvider
    • 6. NavController#setGraph
    • 7. NavInflater#inflate
    • 8. NavController#onGraphCreated
    • 9. NavController#navigate
    • 10. NavGraphNavigator#navigate
    • 11. FragmentNavigator#navigate
    • 11. NavHostFragment#onCreateView
    • 12. NavHostFragment#onViewCreated
  • 解决方案

    前言

    最近项目中使用到了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 内容请搜索自由互联以前的文章或继续浏览下面的相关文章希望大家以后多多支持自由互联!

    上一篇:MobLink Android 快速集成指南
    下一篇:没有了
    网友评论