Jetpack Navigation 分析

1,300 阅读8分钟

Android Jetpack 已经出来很久了,目前在自己的 开源项目 中体验了一把,不得不说很舒服,除了有一些坑之外,这次主要讲解下 Jetpack 中的 NavigationNavigation 主要用来管理 Fragment,方便实现单个 Activity 及 N 多个 FragmentAppNavigation 的使用网上一搜一大把,这里主要通过源码,分析下 Navigation 是如何实现 Fragment 的管理

从布局入手

Navigation 通过指定布局中的 fragment 即可实现,即

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    	<!-- name 指定了根 Fragment,defaultNavHost 用于设置 Fragment 控制系统返回键,
 			navGraph 用于指定 fragment 管理 graph -->
        <fragment
            android:id="@+id/nav_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/demo_navigation" />
    </FrameLayout>

所以我们就从 NavHostFragment 这个类开始入手

NavHostFragment && NavHost

public class NavHostFragment extends Fragment implements NavHost {}

Fragment 实现了 NavHost 接口,这边先跳开下,看下这个接口需要实现的方法

/**
 * A host is a single context or container for navigation via a {@link NavController}.
 */

public interface NavHost {

    /**
     * Returns the {@link NavController navigation controller} for this navigation host.
     *
     * @return this host's navigation controller
     */
    @NonNull
    NavController getNavController();
}

看下官方给该接口的定位,「是个 NavController 的宿主」,NavController 是啥,我们后面再来看,回到 NavHostFragment,首先看下用于 Fragment 初始化常用的几个方法 onInflateonAttachonViewCreatedonCreateView 以及 onCreate

onInflate
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
            @Nullable Bundle savedInstanceState) {
    	// 省略一些非关键代码...
    	// 映射布局的 navGraph 属性,并赋值给 mGraphId,该值用于指定导航图
        final int graphId = navHost.getResourceId(R.styleable.NavHost_navGraph, 0);
        if (graphId != 0) {
            mGraphId = graphId;
        }
        
    	// 省略一些非关键代码...
    	// 映射布局的 defaultNavHost 并赋值给 mDefaultNavHost,该值用于设置是否将返回键控制权给 fragment
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
        if (defaultHost) {
            mDefaultNavHost = true;
        }
    }
onAttach
    @CallSuper
    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        // 如果设置获取返回键控制权的属性为 true,通过 setPrimaryNavigationFragment 方法进行设置
        // 否则,控制权还是在 activity
        if (mDefaultNavHost) {
            requireFragmentManager().beginTransaction()
                    .setPrimaryNavigationFragment(this)
                    .commit();
        }
    }
onViewCreated
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 该方法通过设置 view 的 tag 属性为 controller,后期获取 controller 可能会使用,下同
        Navigation.setViewNavController(view, mNavController);
        
        if (view.getParent() != null) {
            View rootView = (View) view.getParent();
            if (rootView.getId() == getId()) {
                Navigation.setViewNavController(rootView, mNavController);
            }
        }
    }
onCreateView
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
    	// FragmentContainerView 实际是一个 FrameLayout,在该生命周期中,将 fragment 的 id 设置给父布局
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        containerView.setId(getId());
        return containerView;
    }
onCreate
    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        // 初始化 NavController 的一些属性,并将 controller 设置给宿主
        // 包括关联 lifeCycler,返回键的监听属性等
        mNavController = new NavHostController(context);
        // ... 省略一些属性设置代码
        // 在 onCreateNavController 方法中,给 controller 中的 NavigatorProvider 添加了
        // DialogFragmentNavigator 和 FragmentNavigator,这两个类具体实现了什么,先留点悬念,稍后解读
        onCreateNavController(mNavController);

        // 获取 store 的状态,并判断是否要获取返回键控制
        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) {
            mNavController.restoreState(navState);
        }
        
        // 将映射的 navigation 布局设置给 controller
        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);
            }
        }
    }

通过上述的几个方法,将 NavControllerdefaultNavHostNavGraph 的值初始化完成,在 NavHostFragment 中还有个非常重要的方法 findNavController,通过该方法,可以获取到 Fragment 的管理者 NavController

findNavController
    @NonNull
    public static NavController findNavController(@NonNull Fragment fragment) {
        Fragment findFragment = fragment;
        while (findFragment != null) {
            // 如果当前传入的 fragment 就是 NavHostFragment 则直接返回 onCreate 中初始化的 mNavController
            if (findFragment instanceof NavHostFragment) {
                return ((NavHostFragment) findFragment).getNavController();
            }
            
            // 如果不是则通过 onAttach / onCreate 方法中通过 setPrimaryNavigationFragment 方法
            // 设置的 fragment 并返回 mNavController
            Fragment primaryNavFragment = findFragment.requireFragmentManager()
                    .getPrimaryNavigationFragment();
            if (primaryNavFragment instanceof NavHostFragment) {
                return ((NavHostFragment) primaryNavFragment).getNavController();
            }
            // 如果上述都不成立,则获取父级的 Fragment,继续循环去判断获取
            findFragment = findFragment.getParentFragment();
        }

        // Try looking for one associated with the view instead, if applicable
        View view = fragment.getView();
        if (view != null) {
            return Navigation.findNavController(view);
        }
       	throw new IllegalStateException("Fragment " + fragment
                + " does not have a NavController set");
    }

所以,当我们封装 Fragment 基类的时候,即可通过该方法,为所有的 Fragment 寻找其对应的 NavController

在介绍 NavHostFragment 的时候,有个类 NavController 也出现了多次,该 Fragment 就是其宿主,接着就看下 Controller 里面做了什么操作

NavController

NavController 作为整个 AppFragment 管理者,有几个比较重要的方法,包括 SetGraph 设置「导航图」,navigate 跳转 fragment 界面,navigateUp 返回回退栈上个界面,getNavInflater 用于映射 navigation.xml 文件

setGraph

setGraph 重载的方法比较多,但最终会调用 onGraphCreated 方法

    private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
    	// 获取之前保存的状态,并设置状态至 Navigator,Navgator 通过 name 存在 NavigatorProvider 中
    	// 在 NavigatorProvider 中有个 HashMap 用来存储 Navigator
        if (mNavigatorStateToRestore != null) {
            ArrayList<String> navigatorNames = mNavigatorStateToRestore.getStringArrayList(
                    KEY_NAVIGATOR_STATE_NAMES);
            if (navigatorNames != null) {
                for (String name : navigatorNames) {
                    Navigator<?> navigator = mNavigatorProvider.getNavigator(name);
                    Bundle bundle = mNavigatorStateToRestore.getBundle(name);
                    if (bundle != null) {
                        navigator.onRestoreState(bundle);
                    }
                }
            }
        }
    
        if (mBackStackToRestore != null) {
            for (Parcelable parcelable : mBackStackToRestore) {
                // ... 省略一些获取属性的代码
                // ... 设置属性并压入回退栈
                NavBackStackEntry entry = new NavBackStackEntry(mContext, node, args,
                        mLifecycleOwner, mViewModel,
                        state.getUUID(), state.getSavedState());
                mBackStack.add(entry);
            }
            // 更新当前是否可以获取系统返回按钮的控制权
            updateOnBackPressedCallbackEnabled();
            mBackStackToRestore = null;
        }
    
    	// 当设置完「导航图」后,判断是否有 deepLink 属性,如果没有则显示第一个界面
    	// deepLink 用于设置 url,可直接跳转指定的界面
    	// 例如,当收到通知后需要跳转指定界面,则可以通过 deepLink 实现
        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);
            }
        }
    }
navigate

navigate 用于跳转界面,重载的方法也较多,最终调用的内部私有方法 navigate

    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
    	// navOptions 用于设置跳转的动画,pop 时候对应的界面等,具体可以查看 NavOptions 类
        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);
    	
    	// 实际通过 Navigator.navigate 进行跳转
    	// Navigator 是个抽象类,具体实现类有 ActivityNavigator,FragmentNavigator,	
    	// DialogFragmentNavigator,NavGraphNavigator,NoOpNavigator等,且在类头部使用了 Name 注解,
        // 通过 Name 注解,能够在 NavigatorProvider 注册相应的 Navigator
    	// 在 navigation.xml 布局中,通过 Name 对应的值,进行注册即可,
        // 例如注册 fragment 则直接使用 <fragment></fragment> 标签,
        // 同时还有 <activity></activity>,<dialog></dialog>,<navigation></navigation> 等标签
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
    
        if (newDest != null) {
            if (!(newDest instanceof FloatingWindow)) {
                // 如果跳转的界面不是 FloatingWindow 则持续通过 popBackStackInternal 出栈,一直到满足条件
                while (!mBackStack.isEmpty()
                        && mBackStack.peekLast().getDestination() instanceof FloatingWindow
                        && popBackStackInternal(
                                mBackStack.peekLast().getDestination().getId(), true)) {
                    // Keep popping
                }
            }
            
        // ...  省略入栈部分,当跳转完成后,则通知监听
        if (popped || newDest != null) {
            dispatchOnDestinationChanged();
        }
    }
navigateUp

navigateUp 用于回退上个界面,当调用该方法时,会通过回退栈中的数量进行不同处理,如果数量为 1 则会直接 finish 对应的 activity,否则调用 popBackStack 方法,而 popBackStack 最终会调用 popBackStackInternal 方法,该方法返回一个 Boolean 值,用于判断是否出栈成功

boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
         // ...
    	// 列表用于存储需要出栈的 Navigator
        ArrayList<Navigator<?>> popOperations = new ArrayList<>();
        Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
        boolean foundDestination = false;
    
    	// 遍历回退栈的,并将符合出栈条件的 Navigator 放入列表
    	// 如果已经找到了需要的 destination 则打断循环
        while (iterator.hasNext()) {
            NavDestination destination = iterator.next().getDestination();
            Navigator<?> navigator = mNavigatorProvider.getNavigator(
                    destination.getNavigatorName());
            
            if (inclusive || destination.getId() != destinationId) {
                popOperations.add(navigator);
            }
            
            if (destination.getId() == destinationId) {
                foundDestination = true;
                break;
            }
        }
    
    	//...对需要出栈的进行出栈处理
        return popped;
    }
getNavInflater

getNavInflater 通过将 mNavigatorProvider 传给 NavInflater,前面提到过,NavigatorProvider 是用来保存一系列的 Navigator,那么当传入到 NavInflater 中后,该类会对包含的 Navigator 进行解析成一个个 Destination,用于导航跳转,具体如何解析的有兴趣的朋友可以自己看

在上面的 navigate 方法中,提到了实际跳转是通过 Navigator #navigate 进行跳转的,但是 Navigator 是个抽象类,具体的实现由子类完成,因为更多的会使用 fragment,所以我们只看下 FragmentNavigator 类下的 navigate 方法

FragmentNavigator

navigate
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        
    	// ...
    	// 通过 destination 的 className 寻找相应的 Fragment,并设置一些传递的参数
        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();

       	// ...设置一些动画等属性
    
    
        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;
    	
    	// 根据是否是 singleTop,做不同的入栈处理
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                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;
        }
    
        // ...设置一些共享元素
    
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

NavAction && NavDestination

除了上述的几个类以外,Navigation 还有比较重要的就是 NavActionNavDestinationNavAction 中指定了跳转的 DestinationId,额外的携带参数等,可以简单的看成一个实体类,NavDestination 中则包含了各种 NavActionDeepLink 等多种属性,构成了「导航图」上的一个个点。

解决重新创建 Fragment 的坑

Navigation 目前比较大的一个坑就是存在 Fragment 在重新回到界面上的时候会重新创建,既然是坑,那就得解决啊,这边我们借助 ViewModel + LiveData 来完成,封装一个基类

abstract class BaseFragment<VB : ViewDataBinding> : Fragment() {

    protected var mBinding: VB? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        retainInstance = true

        // 保证只会创建一次 view,然后通过 ViewModel + LiveData 对 view 显示内容进行控制
        if (mBinding == null) { 
            mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
            actionsOnViewInflate()
        }
        return mBinding?.root
    }

    // 该方法完整走完一个生命周期只会走一次,可用于该页面进入时网络请求
    open fun actionsOnViewInflate() {}

    abstract fun getLayoutId(): Int
}

但是按照这么封装,在使用 ViewPager + Fragment 的时候会出现重复添加的问题,再做下修改,将添加的先从父布局移除,再添加,就可以完美解决 Navigation 留下的坑

abstract class BaseFragment<VB : ViewDataBinding> : Fragment() {

    protected var mBinding: VB? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        retainInstance = true

        if (mBinding == null) {
            mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
            actionsOnViewInflate()
        }

        // 解决 ViewPager + Fragment 情况下重复添加的问题
        return if (mBinding != null) { 
            mBinding!!.root.apply { (parent as? ViewGroup)?.removeView(this) }
        } else super.onCreateView(inflater, container, savedInstanceState)
    }
}

一张图总结

看了那么多源码,最后用一张比较形象的图来结束吧

nClj9s.png