Jetpack Navigation 剖析

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 我是一名有梦想的教师失落中仍有不忘初心的坚持疲惫时还有继续前行的动力困厄前必有从容面对的勇气不奢求生活中永远充满诗...
    楼顶上的小蚂蚁阅读 370评论 0 1
  • 雨花落,街边风吹过。人在天涯生寂寞,叶入河流任飘泊。心事无处说。
    蛮力阅读 455评论 1 7
  • 我怎么可以变得这么懒惰呢??? 以前,虽然很迷茫,我也是激情满满,动力十足啊!!每天健身,看书,学习,周末骑着自行...
    007王小草阅读 274评论 0 1
  • 这套给我的感觉就是,啊,好韩,好洋气,好喜欢,好便宜但是面料剪裁都好好,要是能重来我选L!打字好累不说了
    Julia_2329阅读 179评论 0 2