天天看点

Jetpack Navigation 分析

Android Jetpack

已经出来很久了,目前在自己的

开源项目

中体验了一把,不得不说很舒服,除了有一些坑之外,这次主要讲解下

Jetpack

中的

Navigation

Navigation

主要用来管理

Fragment

,方便实现单个

Activity

及 N 多个

Fragment

App

Navigation

的使用网上一搜一大把,这里主要通过源码,分析下

Navigation

是如何实现

Fragment

的管理

从布局入手

Navigation

通过指定布局中的

fragment

即可实现,即

frameLabelStart--frameLabelEnd            

所以我们就从

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

初始化常用的几个方法

onInflate

onAttach

onViewCreated

onCreateView

以及

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);
            }
        }
    }           

通过上述的几个方法,将

NavController

defaultNavHost

NavGraph

的值初始化完成,在

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

作为整个

App

Fragment

管理者,有几个比较重要的方法,包括

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

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

还有比较重要的就是

NavAction

NavDestination

NavAction

中指定了跳转的

DestinationId

,额外的携带参数等,可以简单的看成一个实体类,

NavDestination

中则包含了各种

NavAction

DeepLink

等多种属性,构成了「导航图」上的一个个点。

解决重新创建

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)
    }
}           

一张图总结

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

Jetpack Navigation 分析

继续阅读